diff --git a/CLAUDE.md b/CLAUDE.md
index bc6af38c9..d111f8da3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -192,6 +192,11 @@ return useToolOperation({
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
+## Translation Rules
+
+- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
+- Translation files are located in `frontend/public/locales/`
+
## Important Notes
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index 45d36a371..826c48960 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -3079,7 +3079,12 @@
"panMode": "Pan Mode",
"rotateLeft": "Rotate Left",
"rotateRight": "Rotate Right",
- "toggleSidebar": "Toggle Sidebar"
+ "toggleSidebar": "Toggle Sidebar",
+ "exportSelected": "Export Selected Pages",
+ "toggleAnnotations": "Toggle Annotations Visibility",
+ "annotationMode": "Toggle Annotation Mode",
+ "draw": "Draw",
+ "save": "Save"
},
"search": {
"title": "Search PDF",
diff --git a/frontend/src/components/shared/NavigationWarningModal.tsx b/frontend/src/components/shared/NavigationWarningModal.tsx
index e9622d6d4..203e66ff7 100644
--- a/frontend/src/components/shared/NavigationWarningModal.tsx
+++ b/frontend/src/components/shared/NavigationWarningModal.tsx
@@ -8,7 +8,7 @@ interface NavigationWarningModalProps {
}
const NavigationWarningModal = ({
- onApplyAndContinue,
+ onApplyAndContinue: _onApplyAndContinue,
onExportAndContinue
}: NavigationWarningModalProps) => {
@@ -30,13 +30,6 @@ const NavigationWarningModal = ({
confirmNavigation();
};
- const _handleApplyAndContinue = async () => {
- if (onApplyAndContinue) {
- await onApplyAndContinue();
- }
- setHasUnsavedChanges(false);
- confirmNavigation();
- };
const handleExportAndContinue = async () => {
if (onExportAndContinue) {
@@ -85,7 +78,7 @@ const NavigationWarningModal = ({
{/* TODO:: Add this back in when it works */}
- {/* {onApplyAndContinue && (
+ {/* {_onApplyAndContinue && (
+
+ {/* Annotation Controls */}
+
diff --git a/frontend/src/components/shared/rightRail/RightRail.css b/frontend/src/components/shared/rightRail/RightRail.css
index 8d01052a9..d310edc40 100644
--- a/frontend/src/components/shared/rightRail/RightRail.css
+++ b/frontend/src/components/shared/rightRail/RightRail.css
@@ -67,7 +67,7 @@
}
.right-rail-slot.visible {
- max-height: 18rem; /* increased to fit additional controls + divider */
+ max-height: 40rem; /* increased to fit additional controls + divider */
opacity: 1;
}
@@ -77,14 +77,14 @@
opacity: 0;
}
100% {
- max-height: 18rem;
+ max-height: 40rem;
opacity: 1;
}
}
@keyframes rightRailShrinkUp {
0% {
- max-height: 18rem;
+ max-height: 40rem;
opacity: 1;
}
100% {
diff --git a/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx
new file mode 100644
index 000000000..92b778e44
--- /dev/null
+++ b/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx
@@ -0,0 +1,222 @@
+import React, { useState, useEffect } from 'react';
+import { ActionIcon, Popover } from '@mantine/core';
+import { useTranslation } from 'react-i18next';
+import LocalIcon from '../LocalIcon';
+import { Tooltip } from '../Tooltip';
+import { ViewerContext } from '../../../contexts/ViewerContext';
+import { useSignature } from '../../../contexts/SignatureContext';
+import { ColorSwatchButton, ColorPicker } from '../../annotation/shared/ColorPicker';
+import { useFileState, useFileContext } from '../../../contexts/FileContext';
+import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
+import { createProcessedFile } from '../../../contexts/file/fileActions';
+import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
+
+interface ViewerAnnotationControlsProps {
+ currentView: string;
+}
+
+export default function ViewerAnnotationControls({ currentView }: ViewerAnnotationControlsProps) {
+ const { t } = useTranslation();
+ const [selectedColor, setSelectedColor] = useState('#000000');
+ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
+ const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false);
+
+ // Viewer context for PDF controls - safely handle when not available
+ const viewerContext = React.useContext(ViewerContext);
+
+ // Signature context for accessing drawing API
+ const { signatureApiRef } = useSignature();
+
+ // File state for save functionality
+ const { state, selectors } = useFileState();
+ const { actions: fileActions } = useFileContext();
+ const activeFiles = selectors.getFiles();
+
+ // Turn off annotation mode when switching away from viewer
+ useEffect(() => {
+ if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
+ viewerContext.setAnnotationMode(false);
+ }
+ }, [currentView, viewerContext]);
+
+ return (
+ <>
+ {/* Annotation Visibility Toggle */}
+
+ {
+ viewerContext?.toggleAnnotationsVisibility();
+ }}
+ disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode}
+ >
+
+
+
+
+ {/* Annotation Mode Toggle with Drawing Controls */}
+ {viewerContext?.isAnnotationMode ? (
+ // When active: Show color picker on hover
+
setIsHoverColorPickerOpen(true)}
+ onMouseLeave={() => setIsHoverColorPickerOpen(false)}
+ style={{ display: 'inline-flex' }}
+ >
+
setIsHoverColorPickerOpen(false)}
+ position="left"
+ withArrow
+ shadow="md"
+ offset={8}
+ >
+
+ {
+ viewerContext?.toggleAnnotationMode();
+ setIsHoverColorPickerOpen(false); // Close hover color picker when toggling off
+ // Deactivate drawing tool when exiting annotation mode
+ if (signatureApiRef?.current) {
+ try {
+ signatureApiRef.current.deactivateTools();
+ } catch (error) {
+ console.log('Signature API not ready:', error);
+ }
+ }
+ }}
+ disabled={currentView !== 'viewer'}
+ aria-label="Drawing mode active"
+ >
+
+
+
+
+
+
+
Drawing Color
+
{
+ setIsHoverColorPickerOpen(false); // Close hover picker
+ setIsColorPickerOpen(true); // Open main color picker modal
+ }}
+ />
+
+
+
+
+
+ ) : (
+ // When inactive: Show "Draw" tooltip
+
+ {
+ viewerContext?.toggleAnnotationMode();
+ // Activate ink drawing tool when entering annotation mode
+ if (signatureApiRef?.current && currentView === 'viewer') {
+ try {
+ signatureApiRef.current.activateDrawMode();
+ signatureApiRef.current.updateDrawSettings(selectedColor, 2);
+ } catch (error) {
+ console.log('Signature API not ready:', error);
+ }
+ }
+ }}
+ disabled={currentView !== 'viewer'}
+ aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
+ >
+
+
+
+ )}
+
+ {/* Save PDF with Annotations */}
+
+ {
+ if (viewerContext?.exportActions?.saveAsCopy && currentView === 'viewer') {
+ try {
+ const pdfArrayBuffer = await viewerContext.exportActions.saveAsCopy();
+ if (pdfArrayBuffer) {
+ // Create new File object with flattened annotations
+ const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
+
+ // Get the original file name or use a default
+ const originalFileName = activeFiles.length > 0 ? activeFiles[0].name : 'document.pdf';
+ const newFile = new File([blob], originalFileName, { type: 'application/pdf' });
+
+ // Replace the current file in context with the saved version (exact same logic as Sign tool)
+ if (activeFiles.length > 0) {
+ // Generate thumbnail and metadata for the saved file
+ const thumbnailResult = await generateThumbnailWithMetadata(newFile);
+ const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail);
+
+ // Get current file info
+ const currentFileIds = state.files.ids;
+ if (currentFileIds.length > 0) {
+ const currentFileId = currentFileIds[0];
+ const currentRecord = selectors.getStirlingFileStub(currentFileId);
+
+ if (!currentRecord) {
+ console.error('No file record found for:', currentFileId);
+ return;
+ }
+
+ // Create output stub and file (exact same as Sign tool)
+ const outputStub = createNewStirlingFileStub(newFile, undefined, thumbnailResult.thumbnail, processedFileMetadata);
+ const outputStirlingFile = createStirlingFile(newFile, outputStub.id);
+
+ // Replace the original file with the saved version
+ await fileActions.consumeFiles([currentFileId], [outputStirlingFile], [outputStub]);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error saving PDF:', error);
+ }
+ }
+ }}
+ disabled={currentView !== 'viewer'}
+ >
+
+
+
+
+ {/* Color Picker Modal */}
+ setIsColorPickerOpen(false)}
+ selectedColor={selectedColor}
+ onColorChange={(color) => {
+ setSelectedColor(color);
+ // Update drawing tool color if annotation mode is active
+ if (viewerContext?.isAnnotationMode && signatureApiRef?.current && currentView === 'viewer') {
+ try {
+ signatureApiRef.current.updateDrawSettings(color, 2);
+ } catch (error) {
+ console.log('Unable to update drawing settings:', error);
+ }
+ }
+ }}
+ title="Choose Drawing Color"
+ />
+ >
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/viewer/EmbedPdfViewer.tsx b/frontend/src/components/viewer/EmbedPdfViewer.tsx
index 986ac406f..f8a7102fa 100644
--- a/frontend/src/components/viewer/EmbedPdfViewer.tsx
+++ b/frontend/src/components/viewer/EmbedPdfViewer.tsx
@@ -29,16 +29,19 @@ const EmbedPdfViewerContent = ({
const { colorScheme: _colorScheme } = useMantineColorScheme();
const viewerRef = React.useRef(null);
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
- const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState } = useViewer();
+ const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, isAnnotationMode, isAnnotationsVisible } = useViewer();
const scrollState = getScrollState();
const zoomState = getZoomState();
const spreadState = getSpreadState();
- // Check if we're in signature mode
+ // Check if we're in signature mode OR viewer annotation mode
const { selectedTool } = useNavigationState();
const isSignatureMode = selectedTool === 'sign';
+ // Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
+ const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
+
// Get signature context
const { signatureApiRef, historyApiRef } = useSignature();
@@ -186,7 +189,7 @@ const EmbedPdfViewerContent = ({
}
historyApiRef={historyApiRef as React.RefObject}
onSignatureAdded={() => {
diff --git a/frontend/src/components/viewer/LocalEmbedPDF.tsx b/frontend/src/components/viewer/LocalEmbedPDF.tsx
index 2985bae84..842c62945 100644
--- a/frontend/src/components/viewer/LocalEmbedPDF.tsx
+++ b/frontend/src/components/viewer/LocalEmbedPDF.tsx
@@ -42,13 +42,13 @@ import { ExportAPIBridge } from './ExportAPIBridge';
interface LocalEmbedPDFProps {
file?: File | Blob;
url?: string | null;
- enableSignature?: boolean;
+ enableAnnotations?: boolean;
onSignatureAdded?: (annotation: any) => void;
signatureApiRef?: React.RefObject;
historyApiRef?: React.RefObject;
}
-export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
+export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
const [pdfUrl, setPdfUrl] = useState(null);
const [, setAnnotations] = useState>([]);
@@ -93,10 +93,10 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
createPluginRegistration(SelectionPluginPackage),
// Register history plugin for undo/redo (recommended for annotations)
- ...(enableSignature ? [createPluginRegistration(HistoryPluginPackage)] : []),
+ ...(enableAnnotations ? [createPluginRegistration(HistoryPluginPackage)] : []),
// Register annotation plugin (depends on InteractionManager, Selection, History)
- ...(enableSignature ? [createPluginRegistration(AnnotationPluginPackage, {
+ ...(enableAnnotations ? [createPluginRegistration(AnnotationPluginPackage, {
annotationAuthor: 'Digital Signature',
autoCommit: true,
deactivateToolAfterCreate: false,
@@ -194,7 +194,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
{
+ onInitialized={enableAnnotations ? async (registry) => {
const annotationPlugin = registry.getPlugin('annotation');
if (!annotationPlugin || !annotationPlugin.provides) return;
@@ -265,8 +265,8 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
- {enableSignature && }
- {enableSignature && }
+ {enableAnnotations && }
+ {enableAnnotations && }
{/* Annotation layer for signatures (only when enabled) */}
- {enableSignature && (
+ {enableAnnotations && (
);
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx
index 9b541df8a..59fbe43e6 100644
--- a/frontend/src/components/viewer/SignatureAPIBridge.tsx
+++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx
@@ -3,6 +3,7 @@ import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { PdfAnnotationSubtype, PdfStandardFont, PdfTextAlignment, PdfVerticalAlignment, uuidV4 } from '@embedpdf/models';
import { SignParameters } from '../../hooks/tools/sign/useSignParameters';
import { useSignature } from '../../contexts/SignatureContext';
+import { useViewer } from '../../contexts/ViewerContext';
export interface SignatureAPI {
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
@@ -20,11 +21,12 @@ export interface SignatureAPI {
export const SignatureAPIBridge = forwardRef(function SignatureAPIBridge(_, ref) {
const { provides: annotationApi } = useAnnotationCapability();
const { signatureConfig, storeImageData, isPlacementMode } = useSignature();
+ const { isAnnotationMode } = useViewer();
- // Enable keyboard deletion of selected annotations - only when in signature placement mode
+ // Enable keyboard deletion of selected annotations - when in signature placement mode or viewer annotation mode
useEffect(() => {
- if (!annotationApi || !isPlacementMode) return;
+ if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Delete' || event.key === 'Backspace') {
@@ -65,7 +67,7 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
- }, [annotationApi, storeImageData, isPlacementMode]);
+ }, [annotationApi, storeImageData, isPlacementMode, isAnnotationMode]);
useImperativeHandle(ref, () => ({
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
diff --git a/frontend/src/components/viewer/ZoomAPIBridge.tsx b/frontend/src/components/viewer/ZoomAPIBridge.tsx
index 8cb0f4fcc..fa47b1c8a 100644
--- a/frontend/src/components/viewer/ZoomAPIBridge.tsx
+++ b/frontend/src/components/viewer/ZoomAPIBridge.tsx
@@ -15,7 +15,19 @@ export function ZoomAPIBridge() {
if (zoom && !hasSetInitialZoom.current) {
hasSetInitialZoom.current = true;
setTimeout(() => {
- zoom.requestZoom(1.4);
+ try {
+ zoom.requestZoom(1.4);
+ } catch (error) {
+ console.log('Zoom initialization delayed, viewport not ready:', error);
+ // Retry after a longer delay
+ setTimeout(() => {
+ try {
+ zoom.requestZoom(1.4);
+ } catch (retryError) {
+ console.log('Zoom initialization failed:', retryError);
+ }
+ }, 200);
+ }
}, 50);
}
}, [zoom]);
diff --git a/frontend/src/contexts/ViewerContext.tsx b/frontend/src/contexts/ViewerContext.tsx
index 11df4b027..895776f96 100644
--- a/frontend/src/contexts/ViewerContext.tsx
+++ b/frontend/src/contexts/ViewerContext.tsx
@@ -123,6 +123,15 @@ interface ViewerContextType {
isThumbnailSidebarVisible: boolean;
toggleThumbnailSidebar: () => void;
+ // Annotation visibility toggle
+ isAnnotationsVisible: boolean;
+ toggleAnnotationsVisibility: () => void;
+
+ // Annotation/drawing mode for viewer
+ isAnnotationMode: boolean;
+ setAnnotationMode: (enabled: boolean) => void;
+ toggleAnnotationMode: () => void;
+
// State getters - read current state from bridges
getScrollState: () => ScrollState;
getZoomState: () => ZoomState;
@@ -208,6 +217,8 @@ interface ViewerProviderProps {
export const ViewerProvider: React.FC = ({ children }) => {
// UI state - only state directly managed by this context
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
+ const [isAnnotationsVisible, setIsAnnotationsVisible] = useState(true);
+ const [isAnnotationMode, setIsAnnotationModeState] = useState(false);
// Get current navigation state to check if we're in sign mode
useNavigation();
@@ -268,6 +279,18 @@ export const ViewerProvider: React.FC = ({ children }) => {
setIsThumbnailSidebarVisible(prev => !prev);
};
+ const toggleAnnotationsVisibility = () => {
+ setIsAnnotationsVisible(prev => !prev);
+ };
+
+ const setAnnotationMode = (enabled: boolean) => {
+ setIsAnnotationModeState(enabled);
+ };
+
+ const toggleAnnotationMode = () => {
+ setIsAnnotationModeState(prev => !prev);
+ };
+
// State getters - read from bridge refs
const getScrollState = (): ScrollState => {
return bridgeRefs.current.scroll?.state || { currentPage: 1, totalPages: 0 };
@@ -547,6 +570,13 @@ export const ViewerProvider: React.FC = ({ children }) => {
isThumbnailSidebarVisible,
toggleThumbnailSidebar,
+ // Annotation controls
+ isAnnotationsVisible,
+ toggleAnnotationsVisibility,
+ isAnnotationMode,
+ setAnnotationMode,
+ toggleAnnotationMode,
+
// State getters
getScrollState,
getZoomState,
diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts
index 6ae14724f..1c817132a 100644
--- a/frontend/src/contexts/file/fileActions.ts
+++ b/frontend/src/contexts/file/fileActions.ts
@@ -476,7 +476,6 @@ export async function addStirlingFileStubs(
await addFilesMutex.lock();
try {
- if (DEBUG) console.log(`📄 addStirlingFileStubs: Adding ${stirlingFileStubs.length} StirlingFileStubs preserving metadata`);
const existingQuickKeys = buildQuickKeySet(stateRef.current.files.byId);
const validStubs: StirlingFileStub[] = [];
@@ -515,14 +514,12 @@ export async function addStirlingFileStubs(
record.processedFile.totalPages !== record.processedFile.pages.length;
if (needsProcessing) {
- if (DEBUG) console.log(`📄 addStirlingFileStubs: Regenerating processedFile for ${record.name}`);
// Use centralized metadata generation function
const processedFileMetadata = await generateProcessedFileMetadata(stirlingFile);
if (processedFileMetadata) {
record.processedFile = processedFileMetadata;
record.thumbnailUrl = processedFileMetadata.thumbnailUrl; // Update thumbnail if needed
- if (DEBUG) console.log(`📄 addStirlingFileStubs: Regenerated processedFile for ${record.name} with ${processedFileMetadata.totalPages} pages`);
} else {
// Fallback for files that couldn't be processed
if (DEBUG) console.warn(`📄 addStirlingFileStubs: Failed to regenerate processedFile for ${record.name}`);
@@ -541,7 +538,6 @@ export async function addStirlingFileStubs(
// Dispatch ADD_FILES action if we have new files
if (validStubs.length > 0) {
dispatch({ type: 'ADD_FILES', payload: { stirlingFileStubs: validStubs } });
- if (DEBUG) console.log(`📄 addStirlingFileStubs: Successfully added ${validStubs.length} files with preserved metadata`);
}
return loadedFiles;
diff --git a/frontend/src/hooks/useThumbnailGeneration.ts b/frontend/src/hooks/useThumbnailGeneration.ts
index 6a22fbcc9..694fdeeb7 100644
--- a/frontend/src/hooks/useThumbnailGeneration.ts
+++ b/frontend/src/hooks/useThumbnailGeneration.ts
@@ -70,7 +70,6 @@ async function processRequestQueue() {
const pageNumbers = requests.map(req => req.pageNumber);
const arrayBuffer = await file.arrayBuffer();
- console.log(`📸 Batch generating ${requests.length} thumbnails for pages: ${pageNumbers.slice(0, 5).join(', ')}${pageNumbers.length > 5 ? '...' : ''}`);
// Use quickKey for PDF document caching (same metadata, consistent format)
const fileId = createQuickKey(file) as FileId;
@@ -80,9 +79,8 @@ async function processRequestQueue() {
arrayBuffer,
pageNumbers,
{ scale: 1.0, quality: 0.8, batchSize: BATCH_SIZE },
- (progress) => {
+ (_progress) => {
// Optional: Could emit progress events here for UI feedback
- console.log(`📸 Batch progress: ${progress.completed}/${progress.total} thumbnails generated`);
}
);