handleColorChange(color, 'main')}
+ label={t('annotation.changeColor', 'Change Colour')}
+ />
+
+ >
+ );
+ }
+ };
+
+ // Calculate position for portal based on wrapper element
+ useEffect(() => {
+ if (!selected || !annotation || !wrapperRef.current) {
+ setMenuPosition(null);
+ return;
+ }
+
+ const updatePosition = () => {
+ const wrapper = wrapperRef.current;
+ if (!wrapper) {
+ setMenuPosition(null);
+ return;
+ }
+
+ const wrapperRect = wrapper.getBoundingClientRect();
+ // Position menu below the wrapper, centered
+ // Use getBoundingClientRect which gives viewport-relative coordinates
+ // Since we're using fixed positioning in the portal, we don't need to add scroll offsets
+ setMenuPosition({
+ top: wrapperRect.bottom + 8,
+ left: wrapperRect.left + wrapperRect.width / 2,
+ });
+ };
+
+ updatePosition();
+
+ // Update position on scroll/resize
+ window.addEventListener('scroll', updatePosition, true);
+ window.addEventListener('resize', updatePosition);
+
+ return () => {
+ window.removeEventListener('scroll', updatePosition, true);
+ window.removeEventListener('resize', updatePosition);
+ };
+ }, [selected, annotation]);
+
+ // Early return AFTER all hooks have been called
+ if (!selected || !annotation) return null;
+
+ const menuContent = menuPosition ? (
+
+
+ {renderButtons()}
+
+
+ ) : null;
+
+ const textEditorOverlay = isTextEditorOpen && textBoxPosition ? (
+
+
+ ) : null;
+
+ const canClickToEdit = selected && (annotationType === 'text' || annotationType === 'note') && !isTextEditorOpen;
+
+ return (
+ <>
+ {/* Invisible wrapper that provides positioning - uses EmbedPDF's menuWrapperProps */}
+
+ {typeof document !== 'undefined' && menuContent
+ ? createPortal(menuContent, document.body)
+ : null}
+ {typeof document !== 'undefined' && textEditorOverlay
+ ? createPortal(textEditorOverlay, document.body)
+ : null}
+ >
+ );
+}
diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
index 7f004095f..45cfbde78 100644
--- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
+++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx
@@ -50,6 +50,7 @@ import { useTranslation } from 'react-i18next';
import { LinkLayer } from '@app/components/viewer/LinkLayer';
import { TextSelectionHandler } from '@app/components/viewer/TextSelectionHandler';
import { RedactionSelectionMenu } from '@app/components/viewer/RedactionSelectionMenu';
+import { AnnotationSelectionMenu } from '@app/components/viewer/AnnotationSelectionMenu';
import { RedactionPendingTracker, RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker';
import { RedactionAPIBridge } from '@app/components/viewer/RedactionAPIBridge';
import { DocumentPermissionsAPIBridge } from '@app/components/viewer/DocumentPermissionsAPIBridge';
@@ -752,7 +753,7 @@ export function LocalEmbedPDF({ file, url, fileName, enableAnnotations = false,
documentId={documentId}
pageIndex={pageIndex}
selectionOutlineColor="#007ACC"
- selectionMenu={(props) => }
+ selectionMenu={(props) => }
/>
)}
diff --git a/frontend/src/core/tools/Annotate.tsx b/frontend/src/core/tools/Annotate.tsx
index c231124b0..ebf89de6d 100644
--- a/frontend/src/core/tools/Annotate.tsx
+++ b/frontend/src/core/tools/Annotate.tsx
@@ -372,7 +372,6 @@ const Annotate = (_props: BaseToolProps) => {
annotationApiRef,
deriveToolFromAnnotation,
activeToolRef,
- manualToolSwitch,
setActiveTool,
setSelectedTextDraft,
setSelectedFontSize,
diff --git a/frontend/src/core/tools/annotate/AnnotationPanel.tsx b/frontend/src/core/tools/annotate/AnnotationPanel.tsx
index 09d0ebf8f..a0797c4e5 100644
--- a/frontend/src/core/tools/annotate/AnnotationPanel.tsx
+++ b/frontend/src/core/tools/annotate/AnnotationPanel.tsx
@@ -1,7 +1,7 @@
-import { useMemo, useRef, useState } from 'react';
+import { useMemo, useState } from 'react';
import type React from 'react';
import { useTranslation } from 'react-i18next';
-import { Text, Group, ActionIcon, Stack, Slider, Box, Tooltip as MantineTooltip, Button, Textarea, Tooltip, Paper } from '@mantine/core';
+import { Text, Group, ActionIcon, Stack, Slider, Box, Tooltip as MantineTooltip, Button, Tooltip, Paper } from '@mantine/core';
import LocalIcon from '@app/components/shared/LocalIcon';
import { ColorPicker, ColorSwatchButton } from '@app/components/annotation/shared/ColorPicker';
import { ImageUploader } from '@app/components/annotation/shared/ImageUploader';
@@ -111,7 +111,6 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
const { t } = useTranslation();
const [colorPickerTarget, setColorPickerTarget] = useState(null);
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
- const selectedUpdateTimer = useRef | null>(null);
const {
activeTool,
@@ -122,10 +121,6 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
buildToolOptions,
deriveToolFromAnnotation,
selectedAnn,
- selectedTextDraft,
- setSelectedTextDraft,
- selectedFontSize,
- setSelectedFontSize,
annotationApiRef,
viewerContext,
setPlacementMode,
@@ -549,512 +544,6 @@ export function AnnotationPanel(props: AnnotationPanelProps) {
);
- const selectedDerivedTool = selectedAnn?.object ? deriveToolFromAnnotation(selectedAnn.object) : undefined;
-
- const selectedAnnotationControls = selectedAnn && (() => {
- const rawType = selectedAnn.object?.type;
- const toolId = selectedDerivedTool ?? deriveToolFromAnnotation(selectedAnn.object);
- const derivedType =
- toolId === 'highlight' ? 9
- : toolId === 'underline' ? 10
- : toolId === 'squiggly' ? 11
- : toolId === 'strikeout' ? 12
- : toolId === 'line' ? 4
- : toolId === 'square' ? 5
- : toolId === 'circle' ? 6
- : toolId === 'polygon' ? 7
- : toolId === 'polyline' ? 8
- : toolId === 'text' ? 3
- : toolId === 'note' ? 3
- : toolId === 'stamp' ? 13
- : toolId === 'ink' ? 15
- : undefined;
- const type = typeof rawType === 'number' ? rawType : derivedType;
-
- if (toolId && ['highlight', 'underline', 'strikeout', 'squiggly'].includes(toolId)) {
- return (
-
-
- {t('annotation.editTextMarkup', 'Edit Text Markup')}
-
- {t('annotation.color', 'Color')}
- {
- setColorPickerTarget('highlight');
- setIsColorPickerOpen(true);
- }}
- />
-
-
- {t('annotation.opacity', 'Opacity')}
- {
- annotationApiRef?.current?.updateAnnotation?.(
- selectedAnn.object?.pageIndex ?? 0,
- selectedAnn.object?.id,
- { opacity: value / 100 }
- );
- }}
- />
-
-
-
- );
- }
-
- if (type === 15 || toolId === 'inkHighlighter' || toolId === 'ink') {
- const isHighlighter = toolId === 'inkHighlighter';
- const thicknessValue =
- selectedAnn.object?.strokeWidth ??
- selectedAnn.object?.borderWidth ??
- selectedAnn.object?.lineWidth ??
- selectedAnn.object?.thickness ??
- (isHighlighter ? freehandHighlighterWidth : inkWidth);
- const colorValue = selectedAnn.object?.color ?? (isHighlighter ? highlightColor : inkColor);
- const opacityValue = Math.round(((selectedAnn.object?.opacity ?? 1) * 100) || (isHighlighter ? highlightOpacity : 100));
- return (
-
-
-
- {isHighlighter ? t('annotation.freehandHighlighter', 'Freehand Highlighter') : t('annotation.editInk', 'Edit Pen')}
-
-
- {t('annotation.color', 'Color')}
- {
- setColorPickerTarget(isHighlighter ? 'highlight' : 'ink');
- setIsColorPickerOpen(true);
- }}
- />
-
- {isHighlighter && (
-
- {t('annotation.opacity', 'Opacity')}
- {
- setHighlightOpacity(value);
- annotationApiRef?.current?.updateAnnotation?.(
- selectedAnn.object?.pageIndex ?? 0,
- selectedAnn.object?.id,
- { opacity: value / 100 }
- );
- }}
- />
-
- )}
-
- {t('annotation.strokeWidth', 'Width')}
- {
- annotationApiRef?.current?.updateAnnotation?.(
- selectedAnn.object?.pageIndex ?? 0,
- selectedAnn.object?.id,
- {
- strokeWidth: value,
- borderWidth: value,
- lineWidth: value,
- thickness: value,
- }
- );
- if (isHighlighter) {
- setFreehandHighlighterWidth?.(value);
- } else {
- setInkWidth(value);
- }
- }}
- />
-
-
-
- );
- }
-
- if (type === 3 || toolId === 'text' || toolId === 'note') {
- const isNote = toolId === 'note';
- const selectedBackground =
- selectedAnn.object?.backgroundColor ??
- (isNote ? noteBackgroundColor || '#ffffff' : textBackgroundColor || '#ffffff');
- const alignValue = selectedAnn.object?.textAlign;
- const currentAlign =
- typeof alignValue === 'number'
- ? alignValue === 1
- ? 'center'
- : alignValue === 2
- ? 'right'
- : 'left'
- : alignValue === 'center'
- ? 'center'
- : alignValue === 'right'
- ? 'right'
- : 'left';
-
- return (
-
-
- {isNote ? t('annotation.editNote', 'Edit Sticky Note') : t('annotation.editText', 'Edit Text Box')}
-
- {t('annotation.color', 'Color')}
- {
- setColorPickerTarget('text');
- setIsColorPickerOpen(true);
- }}
- />
-
-
- {t('annotation.backgroundColor', 'Background color')}
-
- {
- setColorPickerTarget(isNote ? 'noteBackground' : 'textBackground');
- setIsColorPickerOpen(true);
- }}
- />
-
-
-
-
-
- );
- }
-
- if (type === 13 || toolId === 'stamp') {
- const imageSrc = selectedAnn.object?.imageSrc || selectedAnn.object?.data || selectedAnn.object?.url;
- return (
-
-
- {t('annotation.stamp', 'Add Image')}
- {imageSrc ? (
-
- {t('annotation.imagePreview', 'Preview')}
-
-
- ) : (
-
- {t('annotation.unsupportedType', 'This annotation type is not fully supported for editing.')}
-
- )}
-
- {t('annotation.editStampHint', 'To change the image, delete this stamp and add a new one.')}
-
-
-
- );
- }
-
- if ((type !== undefined && [4, 8].includes(type)) || toolId === 'line' || toolId === 'polyline') {
- return (
-
-
- {t('annotation.editLine', 'Edit Line')}
-
- {t('annotation.color', 'Color')}
- {
- setColorPickerTarget('shapeStroke');
- setIsColorPickerOpen(true);
- }}
- />
-
-
- {t('annotation.opacity', 'Opacity')}
- {
- annotationApiRef?.current?.updateAnnotation?.(
- selectedAnn.object?.pageIndex ?? 0,
- selectedAnn.object?.id,
- { opacity: value / 100 }
- );
- }}
- />
-
-
- {t('annotation.strokeWidth', 'Width')}
- {
- annotationApiRef?.current?.updateAnnotation?.(
- selectedAnn.object?.pageIndex ?? 0,
- selectedAnn.object?.id,
- {
- borderWidth: value,
- strokeWidth: value,
- lineWidth: value,
- }
- );
- setShapeThickness(value);
- }}
- />
-
-
-
- );
- }
-
- if ((type !== undefined && [5, 6, 7].includes(type)) || toolId === 'square' || toolId === 'circle' || toolId === 'polygon') {
- const shapeName = type === 5 ? 'Square' : type === 6 ? 'Circle' : 'Polygon';
- const strokeColorValue = selectedAnn.object?.strokeColor ?? shapeStrokeColor;
- const fillColorValue = selectedAnn.object?.color ?? shapeFillColor;
- const opacityValue = Math.round(((selectedAnn.object?.opacity ?? shapeOpacity / 100) * 100) || 100);
- const pageIndex = selectedAnn.object?.pageIndex ?? 0;
- const annId = selectedAnn.object?.id;
- return (
-
-
- {t(`annotation.edit${shapeName}`, `Edit ${shapeName}`)}
-
-
- {t('annotation.strokeColor', 'Stroke Color')}
- {
- setColorPickerTarget('shapeStroke');
- setIsColorPickerOpen(true);
- }}
- />
-
-
- {t('annotation.fillColor', 'Fill Color')}
- {
- setColorPickerTarget('shapeFill');
- setIsColorPickerOpen(true);
- }}
- />
-
-
-
- {t('annotation.opacity', 'Opacity')}
- {
- setShapeOpacity(value);
- setShapeStrokeOpacity(value);
- setShapeFillOpacity(value);
- if (annId) {
- annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, {
- opacity: value / 100,
- strokeOpacity: value / 100,
- fillOpacity: value / 100,
- });
- }
- }}
- />
-
-
-
- {t('annotation.strokeWidth', 'Stroke')}
- {
- if (annId) {
- annotationApiRef?.current?.updateAnnotation?.(pageIndex, annId, {
- borderWidth: value,
- strokeWidth: value,
- lineWidth: value,
- });
- }
- setShapeThickness(value);
- }}
- />
-
-
-
-
-
- );
- }
-
- return (
-
-
- {t('annotation.editSelected', 'Edit Annotation')}
- {t('annotation.unsupportedType', 'This annotation type is not fully supported for editing.')}
-
-
- );
- })();
-
const colorPickerComponent = (
- {activeTool !== 'select' && defaultStyleControls}
-
- {activeTool === 'select' && selectedAnn && selectedAnnotationControls}
-
- {activeTool === 'select' && !selectedAnn && defaultStyleControls}
+ {activeTool === 'stamp' && defaultStyleControls}
{colorPickerComponent}
diff --git a/frontend/src/core/tools/annotate/useAnnotationSelection.ts b/frontend/src/core/tools/annotate/useAnnotationSelection.ts
index 020408b59..382ae2f80 100644
--- a/frontend/src/core/tools/annotate/useAnnotationSelection.ts
+++ b/frontend/src/core/tools/annotate/useAnnotationSelection.ts
@@ -5,7 +5,6 @@ interface UseAnnotationSelectionParams {
annotationApiRef: React.RefObject;
deriveToolFromAnnotation: (annotation: any) => AnnotationToolId | undefined;
activeToolRef: React.MutableRefObject;
- manualToolSwitch: React.MutableRefObject;
setActiveTool: (toolId: AnnotationToolId) => void;
setSelectedTextDraft: (text: string) => void;
setSelectedFontSize: (size: number) => void;
@@ -34,6 +33,7 @@ interface UseAnnotationSelectionParams {
const MARKUP_TOOL_IDS = ['highlight', 'underline', 'strikeout', 'squiggly'] as const;
const DRAWING_TOOL_IDS = ['ink', 'inkHighlighter'] as const;
+const STAY_ACTIVE_TOOL_IDS = [...MARKUP_TOOL_IDS, ...DRAWING_TOOL_IDS] as const;
const isTextMarkupAnnotation = (annotation: any): boolean => {
const toolId =
@@ -55,6 +55,9 @@ const isTextMarkupAnnotation = (annotation: any): boolean => {
};
const shouldStayOnPlacementTool = (annotation: any, derivedTool?: string | null | undefined): boolean => {
+ // Text markup tools (highlight, underline, strikeout, squiggly) and drawing tools (ink, inkHighlighter) stay active
+ // All other tools switch to select mode after placement
+
const toolId =
derivedTool ||
annotation?.customData?.annotationToolId ||
@@ -62,12 +65,17 @@ const shouldStayOnPlacementTool = (annotation: any, derivedTool?: string | null
annotation?.object?.customData?.annotationToolId ||
annotation?.object?.customData?.toolId;
- if (toolId && (MARKUP_TOOL_IDS.includes(toolId as any) || DRAWING_TOOL_IDS.includes(toolId as any))) {
+ // Check if it's a tool that should stay active
+ if (toolId && STAY_ACTIVE_TOOL_IDS.includes(toolId as any)) {
return true;
}
- const type = annotation?.type ?? annotation?.object?.type;
- if (typeof type === 'number' && type === 15) return true; // ink family
- if (isTextMarkupAnnotation(annotation)) return true;
+
+ // Check if it's a markup annotation by type/subtype
+ if (isTextMarkupAnnotation(annotation)) {
+ return true;
+ }
+
+ // All other tools (text, note, shapes, lines, stamps) switch to select
return false;
};
@@ -75,7 +83,6 @@ export function useAnnotationSelection({
annotationApiRef,
deriveToolFromAnnotation,
activeToolRef,
- manualToolSwitch,
setActiveTool,
setSelectedTextDraft,
setSelectedFontSize,
@@ -226,8 +233,8 @@ export function useAnnotationSelection({
},
[
activeToolRef,
+ annotationApiRef,
deriveToolFromAnnotation,
- manualToolSwitch,
setActiveTool,
setInkWidth,
setNoteBackgroundColor,
@@ -252,7 +259,6 @@ export function useAnnotationSelection({
setShapeFillOpacity,
setTextAlignment,
setFreehandHighlighterWidth,
- shouldStayOnPlacementTool,
]
);
@@ -304,9 +310,7 @@ export function useAnnotationSelection({
const tool =
deriveToolFromAnnotation((eventAnn as any)?.object ?? eventAnn ?? api.getSelectedAnnotation?.()) ||
currentTool;
- const stayOnPlacement =
- shouldStayOnPlacementTool(eventAnn, tool) ||
- (tool ? DRAWING_TOOL_IDS.includes(tool as any) : false);
+ const stayOnPlacement = shouldStayOnPlacementTool(eventAnn, tool);
if (activeToolRef.current !== 'select' && !stayOnPlacement) {
activeToolRef.current = 'select';
setActiveTool('select');
@@ -318,9 +322,7 @@ export function useAnnotationSelection({
applySelectionFromAnnotation(selected ?? eventAnn ?? null);
const derivedAfter =
deriveToolFromAnnotation((selected as any)?.object ?? selected ?? eventAnn ?? null) || activeToolRef.current;
- const stayOnPlacementAfter =
- shouldStayOnPlacementTool(selected ?? eventAnn ?? null, derivedAfter) ||
- (derivedAfter ? DRAWING_TOOL_IDS.includes(derivedAfter as any) : false);
+ const stayOnPlacementAfter = shouldStayOnPlacementTool(selected ?? eventAnn ?? null, derivedAfter);
if (activeToolRef.current !== 'select' && !stayOnPlacementAfter) {
activeToolRef.current = 'select';
setActiveTool('select');