make clicking on comments open the comments sidebar and more (#6174)

make clicking on comments open the comments sidebar and add a button to
create comments into the empty state of the comments sidebar
<img width="2056" height="1081" alt="Screenshot 2026-04-23 at 12 39
50 PM"
src="https://github.com/user-attachments/assets/6f15484d-d04f-4900-92c6-b2cc397d6d08"
/>

<img width="627" height="396" alt="Screenshot 2026-04-23 at 12 40 06 PM"
src="https://github.com/user-attachments/assets/509e5526-0082-4fc6-a98f-829bb4c1baf2"
/>
This commit is contained in:
EthanHealy01
2026-04-23 17:22:47 +01:00
committed by GitHub
parent 177c776658
commit 7c42d8018a
6 changed files with 237 additions and 49 deletions
@@ -1,6 +1,6 @@
import { Group } from "@mantine/core"; import { Group } from "@mantine/core";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useEffect, useState, useRef, useCallback } from "react"; import { useEffect, useState, useRef, useCallback, useMemo } from "react";
import { useAnnotation } from "@embedpdf/plugin-annotation/react"; import { useAnnotation } from "@embedpdf/plugin-annotation/react";
import type { TrackedAnnotation } from "@embedpdf/plugin-annotation"; import type { TrackedAnnotation } from "@embedpdf/plugin-annotation";
import { import {
@@ -47,7 +47,7 @@ function AnnotationSelectionMenuInner({
}: AnnotationSelectionMenuProps & { documentId: string }) { }: AnnotationSelectionMenuProps & { documentId: string }) {
const annotation = context?.annotation; const annotation = context?.annotation;
const pageIndex = context?.pageIndex; const pageIndex = context?.pageIndex;
const { provides } = useAnnotation(documentId); const { state, provides } = useAnnotation(documentId);
const { scrollActions, requestCommentFocus } = useViewer(); const { scrollActions, requestCommentFocus } = useViewer();
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = useState<{ const [menuPosition, setMenuPosition] = useState<{
@@ -65,17 +65,52 @@ function AnnotationSelectionMenuInner({
wrapperRef, wrapperRef,
}); });
// Read isInSidebar from live EmbedPDF state rather than annotation.object, which can be
// stale after updateAnnotation() is called while the annotation is selected.
// Also checks non-empty contents: customData.isComment is not persisted to PDF, but
// contents is a standard PDF field and survives save/reload.
const isInSidebar = useMemo(() => {
const annId = (annotation?.object as AnnotationObject | undefined)?.id;
if (!annId) return false;
for (const tracked of Object.values(state.byUid)) {
const obj = tracked.object;
if (obj.id !== annId) continue;
const { type } = obj;
// TEXT and CARET are standalone comment annotations — they use CommentButton,
// not AttachCommentButton, so isInSidebar is irrelevant for them.
if (
type === PdfAnnotationSubtype.TEXT ||
type === PdfAnnotationSubtype.CARET
)
return false;
// customData is a runtime field EmbedPDF adds but doesn't declare in its TS types
const customData = (
obj as unknown as { customData?: Record<string, unknown> }
).customData;
const isExplicit = customData?.isComment === true;
// customData (incl. toolId/isComment) is not persisted to PDF; contents is.
// Any non-TEXT/FreeText/CARET annotation with non-empty contents has a comment.
const hasContents =
type !== PdfAnnotationSubtype.FREETEXT &&
!obj.inReplyToId &&
(obj.contents ?? "").trim().length > 0;
return isExplicit || hasContents;
}
return false;
}, [state, annotation?.object]);
// Auto-open the comments sidebar when a comment annotation is selected // Auto-open the comments sidebar when a comment annotation is selected
useEffect(() => { useEffect(() => {
const annObj = annotation?.object as AnnotationObject | undefined; const annObj = annotation?.object as AnnotationObject | undefined;
const annId = annObj?.id; const annId = annObj?.id;
if (!selected || !annId || pageIndex === undefined) return; if (!selected || !annId || pageIndex === undefined) return;
const toolId = annObj?.customData?.toolId;
const annType = annObj?.type; const annType = annObj?.type;
// TEXT (type 1) = textComment; CARET (type 14) = insertText/replaceText.
// These are always comment annotations regardless of toolId (lost after reload).
const isComment = const isComment =
(annType === PdfAnnotationSubtype.TEXT && toolId === "textComment") || annType === PdfAnnotationSubtype.TEXT ||
(annType === PdfAnnotationSubtype.CARET && annType === PdfAnnotationSubtype.CARET ||
(toolId === "insertText" || toolId === "replaceText")); isInSidebar;
if (!isComment) return; if (!isComment) return;
requestCommentFocus( requestCommentFocus(
documentId, documentId,
@@ -83,7 +118,7 @@ function AnnotationSelectionMenuInner({
annId, annId,
(annObj?.contents ?? "").trim().length > 0, (annObj?.contents ?? "").trim().length > 0,
); );
}, [selected, annotation?.object]); }, [selected, annotation?.object, isInSidebar]);
// Click outside to deselect // Click outside to deselect
useEffect(() => { useEffect(() => {
@@ -170,6 +205,7 @@ function AnnotationSelectionMenuInner({
<Group gap="sm" wrap="nowrap" justify="center"> <Group gap="sm" wrap="nowrap" justify="center">
<AnnotationTypeButtons <AnnotationTypeButtons
{...handlers} {...handlers}
isInSidebar={isInSidebar}
annotation={annotation} annotation={annotation}
documentId={documentId} documentId={documentId}
pageIndex={pageIndex} pageIndex={pageIndex}
@@ -58,9 +58,13 @@ export function AnnotationTypeButtons(props: AnnotationTypeButtonsProps) {
onCommentColorChange, onCommentColorChange,
} = props; } = props;
const attachCommentButton = ( // When a comment is attached, show the same chat-bubble "View comment" button used by
// standalone comment annotations. When no comment, show the "Add comment" attach button.
const attachCommentButton = isInSidebar ? (
<CommentButton hasContent={hasCommentContent} onClick={onViewComment} />
) : (
<AttachCommentButton <AttachCommentButton
isInSidebar={isInSidebar} isInSidebar={false}
onView={onViewComment} onView={onViewComment}
onAdd={onAddToSidebar} onAdd={onAddToSidebar}
/> />
@@ -221,10 +225,6 @@ export function AnnotationTypeButtons(props: AnnotationTypeButtonsProps) {
return ( return (
<> <>
{attachCommentButton} {attachCommentButton}
<CommentButton
hasContent={hasCommentContent}
onClick={onViewComment}
/>
<LinkButton <LinkButton
firstLinkTarget={firstLinkTarget} firstLinkTarget={firstLinkTarget}
onGoToLink={onGoToLink} onGoToLink={onGoToLink}
@@ -22,13 +22,37 @@ import EditIcon from "@mui/icons-material/Edit";
import VisibilityIcon from "@mui/icons-material/Visibility"; import VisibilityIcon from "@mui/icons-material/Visibility";
import { useAnnotation } from "@embedpdf/plugin-annotation/react"; import { useAnnotation } from "@embedpdf/plugin-annotation/react";
import { getSidebarAnnotationsWithRepliesGroupedByPage } from "@embedpdf/plugin-annotation"; import { getSidebarAnnotationsWithRepliesGroupedByPage } from "@embedpdf/plugin-annotation";
import { PdfAnnotationSubtype, PdfAnnotationReplyType } from "@embedpdf/models"; import {
PdfAnnotationSubtype,
PdfAnnotationReplyType,
type PdfAnnotationObject,
type PdfTextAnnoObject,
} from "@embedpdf/models";
import { useCommentAuthor } from "@app/contexts/CommentAuthorContext"; import { useCommentAuthor } from "@app/contexts/CommentAuthorContext";
import { useViewer } from "@app/contexts/ViewerContext"; import { useViewer } from "@app/contexts/ViewerContext";
import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
import { useAnnotation as useAnnotationContext } from "@app/contexts/AnnotationContext";
import LocalIcon from "@app/components/shared/LocalIcon"; import LocalIcon from "@app/components/shared/LocalIcon";
const SIDEBAR_WIDTH = "18rem"; const SIDEBAR_WIDTH = "18rem";
/** PDF subtypes that are inherently standalone comment annotations (not linked to other annotations). */
const STANDALONE_COMMENT_SUBTYPES = new Set([
PdfAnnotationSubtype.TEXT,
PdfAnnotationSubtype.FREETEXT,
PdfAnnotationSubtype.CARET,
]);
function isStandaloneCommentType(type: number | undefined): boolean {
return (
type !== undefined &&
STANDALONE_COMMENT_SUBTYPES.has(type as PdfAnnotationSubtype)
);
}
const ANNOTATE_PANEL_ID = "annotate" as const;
const TEXT_COMMENT_TOOL_ID = "textComment" as const;
/** Format annotation date for display (e.g. "Mar 11, 6:05 PM"). */ /** Format annotation date for display (e.g. "Mar 11, 6:05 PM"). */
function formatCommentDate(obj: any): string { function formatCommentDate(obj: any): string {
const raw = const raw =
@@ -143,8 +167,20 @@ function isCommentAnnotation(ann: any): boolean {
return true; return true;
// Any annotation explicitly added to comments via the "Add comment" button // Any annotation explicitly added to comments via the "Add comment" button
if (ann?.customData?.isComment === true) return true; if (ann?.customData?.isComment === true) return true;
// CARET (type 14) = insertText/replaceText; TEXT (type 1) = textComment const type = ann?.type;
if (!toolId && (ann?.type === 14 || ann?.type === 1)) return true; // Standalone comment types (TEXT, FREETEXT, CARET) without a toolId are always comments
if (!toolId && isStandaloneCommentType(type)) return true;
// Non-standalone annotations with non-empty contents: customData (including isComment and
// toolId) is NOT persisted to PDF, but `contents` is a standard PDF field and survives
// save/reload. Exclude standalone comment types (TEXT, FREETEXT, CARET) which use
// `contents` for their own annotation text. Exclude replies.
if (
type !== undefined &&
!isStandaloneCommentType(type) &&
!ann?.inReplyToId &&
(ann?.contents ?? "").trim().length > 0
)
return true;
return false; return false;
} }
@@ -213,6 +249,8 @@ export function CommentsSidebar({
} = useViewer() ?? {}; } = useViewer() ?? {};
const scrollViewportRef = useRef<HTMLDivElement | null>(null); const scrollViewportRef = useRef<HTMLDivElement | null>(null);
const { state, provides } = useAnnotation(documentId); const { state, provides } = useAnnotation(documentId);
const { handleToolSelectForced } = useToolWorkflow();
const { activateAnnotationToolRef } = useAnnotationContext();
const [draftContents, setDraftContents] = useState<Record<string, string>>( const [draftContents, setDraftContents] = useState<Record<string, string>>(
{}, {},
); );
@@ -324,8 +362,8 @@ export function CommentsSidebar({
const all = getSidebarAnnotationsWithRepliesGroupedByPage(state) ?? {}; const all = getSidebarAnnotationsWithRepliesGroupedByPage(state) ?? {};
const filtered: typeof all = {}; const filtered: typeof all = {};
for (const [page, entries] of Object.entries(all)) { for (const [page, entries] of Object.entries(all)) {
const commentEntries = (entries as typeof entries).filter((e) => const commentEntries = entries.filter((e) =>
isCommentAnnotation((e as any).annotation?.object), isCommentAnnotation(e.annotation.object),
); );
if (commentEntries.length > 0) { if (commentEntries.length > 0) {
filtered[Number(page)] = commentEntries; filtered[Number(page)] = commentEntries;
@@ -341,12 +379,11 @@ export function CommentsSidebar({
// state is AnnotationDocumentState — selectedUids are keys in byUid, and may equal id. // state is AnnotationDocumentState — selectedUids are keys in byUid, and may equal id.
const selectedAnnotationIds = useMemo(() => { const selectedAnnotationIds = useMemo(() => {
const selectedUids: string[] = state?.selectedUids ?? []; const selectedUids: string[] = state?.selectedUids ?? [];
const byUid: Record<string, any> = (state as any)?.byUid ?? {};
const ids = new Set<string>(); const ids = new Set<string>();
for (const uid of selectedUids) { for (const uid of selectedUids) {
// uid itself may be the annotation id // uid itself may be the annotation id
ids.add(uid); ids.add(uid);
const annId = byUid[uid]?.object?.id; const annId = state.byUid[uid]?.object.id;
if (annId) ids.add(annId); if (annId) ids.add(annId);
} }
return ids; return ids;
@@ -383,12 +420,12 @@ export function CommentsSidebar({
} | null>(null); } | null>(null);
const isLinkedAnnotation = (ann: any) => { const isLinkedAnnotation = (ann: any) => {
const toolId = ann?.customData?.toolId ?? ann?.customData?.annotationToolId; const type = ann?.type;
if (isStandaloneCommentType(type)) return false;
if (ann?.inReplyToId) return false;
return ( return (
ann?.customData?.isComment === true && ann?.customData?.isComment === true ||
toolId !== "textComment" && (type !== undefined && (ann?.contents ?? "").trim().length > 0)
toolId !== "insertText" &&
toolId !== "replaceText"
); );
}; };
@@ -408,7 +445,13 @@ export function CommentsSidebar({
const { pageIndex, id, ann } = deleteModal; const { pageIndex, id, ann } = deleteModal;
const existing = (ann?.customData ?? {}) as Record<string, unknown>; const existing = (ann?.customData ?? {}) as Record<string, unknown>;
const { isComment: _removed, ...rest } = existing; const { isComment: _removed, ...rest } = existing;
provides.updateAnnotation(pageIndex, id, { customData: rest } as any); // Also clear contents: the contents field is the persisted signal for
// post-reload linked annotations, so clearing it removes the annotation
// from the sidebar (contents is not visually rendered on ink/shape/markup types).
provides.updateAnnotation(pageIndex, id, {
customData: rest,
contents: "",
} as unknown as Partial<PdfAnnotationObject>);
setDeleteModal(null); setDeleteModal(null);
}, [deleteModal, provides]); }, [deleteModal, provides]);
@@ -443,7 +486,7 @@ export function CommentsSidebar({
origin: { x: 0, y: 0 }, origin: { x: 0, y: 0 },
size: { width: 1, height: 1 }, size: { width: 1, height: 1 },
}; };
provides.createAnnotation(pageIndex, { const reply: PdfTextAnnoObject = {
type: PdfAnnotationSubtype.TEXT, type: PdfAnnotationSubtype.TEXT,
id: `reply-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, id: `reply-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
pageIndex, pageIndex,
@@ -452,7 +495,8 @@ export function CommentsSidebar({
inReplyToId: parentId, inReplyToId: parentId,
replyType: PdfAnnotationReplyType.Reply, replyType: PdfAnnotationReplyType.Reply,
author: displayName, author: displayName,
} as any); };
provides.createAnnotation(pageIndex, reply);
setReplyDrafts((prev) => ({ ...prev, [key]: "" })); setReplyDrafts((prev) => ({ ...prev, [key]: "" }));
}, },
[provides, replyDrafts, displayName], [provides, replyDrafts, displayName],
@@ -476,6 +520,13 @@ export function CommentsSidebar({
[provides, displayName], [provides, displayName],
); );
const handleAddComment = useCallback(() => {
handleToolSelectForced(ANNOTATE_PANEL_ID);
requestAnimationFrame(() => {
activateAnnotationToolRef.current?.(TEXT_COMMENT_TOOL_ID);
});
}, [handleToolSelectForced, activateAnnotationToolRef]);
if (!visible) return null; if (!visible) return null;
return ( return (
@@ -510,19 +561,49 @@ export function CommentsSidebar({
height="1.25rem" height="1.25rem"
style={{ color: "var(--mantine-color-dimmed)", flexShrink: 0 }} style={{ color: "var(--mantine-color-dimmed)", flexShrink: 0 }}
/> />
<Text fw={600} size="sm" tt="uppercase" lts={0.5}> <Text fw={600} size="sm" tt="uppercase" lts={0.5} style={{ flex: 1 }}>
{t("viewer.comments.title", "Comments")} {t("viewer.comments.title", "Comments")}
</Text> </Text>
{totalCount > 0 && (
<Tooltip label={t("viewer.comments.addComment", "Add comment")}>
<ActionIcon
variant="subtle"
size="sm"
color="gray"
onClick={handleAddComment}
>
<LocalIcon icon="add" width="1.25rem" height="1.25rem" />
</ActionIcon>
</Tooltip>
)}
</div> </div>
<ScrollArea style={{ flex: 1 }}> <ScrollArea style={{ flex: 1 }}>
<Stack p="sm" gap="md"> <Stack p="sm" gap="md">
{totalCount === 0 ? ( {totalCount === 0 ? (
<Text size="sm" c="dimmed"> <Stack align="center" gap="sm" py="lg">
<LocalIcon
icon="comment"
width="2rem"
height="2rem"
style={{ color: "var(--mantine-color-dimmed)" }}
/>
<Text size="sm" c="dimmed" ta="center">
{t( {t(
"viewer.comments.hint", "viewer.comments.hint",
"Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.", "Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.",
)} )}
</Text> </Text>
<Button
variant="light"
size="xs"
onClick={handleAddComment}
leftSection={
<LocalIcon icon="add" width="1rem" height="1rem" />
}
>
{t("viewer.comments.addComment", "Add comment")}
</Button>
</Stack>
) : ( ) : (
pageNumbers.map((pageIndex) => { pageNumbers.map((pageIndex) => {
const entries = byPage[pageIndex] ?? []; const entries = byPage[pageIndex] ?? [];
@@ -27,6 +27,32 @@ export type AnnotationType =
| "stamp" | "stamp"
| "unknown"; | "unknown";
const TEXT_MARKUP_SUBTYPES = [
PdfAnnotationSubtype.HIGHLIGHT,
PdfAnnotationSubtype.UNDERLINE,
PdfAnnotationSubtype.SQUIGGLY,
PdfAnnotationSubtype.STRIKEOUT,
];
const SHAPE_SUBTYPES = [
PdfAnnotationSubtype.SQUARE,
PdfAnnotationSubtype.CIRCLE,
PdfAnnotationSubtype.POLYGON,
];
const LINE_SUBTYPES = [
PdfAnnotationSubtype.LINE,
PdfAnnotationSubtype.POLYLINE,
];
const STROKE_COLOR_SUBTYPES = [
PdfAnnotationSubtype.LINE,
PdfAnnotationSubtype.SQUARE,
PdfAnnotationSubtype.CIRCLE,
PdfAnnotationSubtype.POLYGON,
PdfAnnotationSubtype.POLYLINE,
];
export type FirstLinkTarget = export type FirstLinkTarget =
| { type: "uri"; uri: string } | { type: "uri"; uri: string }
| { type: "goto"; pageIndex: number }; | { type: "goto"; pageIndex: number };
@@ -134,17 +160,36 @@ export function useAnnotationMenuHandlers({
const toolId = (annotation?.object as AnnotationObject | undefined) const toolId = (annotation?.object as AnnotationObject | undefined)
?.customData?.toolId; ?.customData?.toolId;
if (type !== undefined && [9, 10, 11, 12].includes(type)) if (
type !== undefined &&
TEXT_MARKUP_SUBTYPES.includes(type as PdfAnnotationSubtype)
)
return "textMarkup"; return "textMarkup";
if (type === 15) if (type === PdfAnnotationSubtype.INK)
return toolId === "inkHighlighter" ? "inkHighlighter" : "ink"; return toolId === "inkHighlighter" ? "inkHighlighter" : "ink";
if (type === 1 && toolId === "textComment") return "comment"; // TEXT and CARET fall back to type alone after save/reload (customData.toolId is absent).
if (type === 14 && (toolId === "insertText" || toolId === "replaceText")) if (
type === PdfAnnotationSubtype.TEXT &&
(!toolId || toolId === "textComment")
)
return "comment"; return "comment";
if (type === 3) return "note"; if (
if (type !== undefined && [5, 6, 7].includes(type)) return "shape"; type === PdfAnnotationSubtype.CARET &&
if (type !== undefined && [4, 8].includes(type)) return "line"; (!toolId || toolId === "insertText" || toolId === "replaceText")
if (type === 13) return "stamp"; )
return "comment";
if (type === PdfAnnotationSubtype.FREETEXT) return "note";
if (
type !== undefined &&
SHAPE_SUBTYPES.includes(type as PdfAnnotationSubtype)
)
return "shape";
if (
type !== undefined &&
LINE_SUBTYPES.includes(type as PdfAnnotationSubtype)
)
return "line";
if (type === PdfAnnotationSubtype.STAMP) return "stamp";
return "unknown"; return "unknown";
}, [annotation]); }, [annotation]);
@@ -174,8 +219,12 @@ export function useAnnotationMenuHandlers({
const currentColor = (() => { const currentColor = (() => {
if (!obj) return "#000000"; if (!obj) return "#000000";
const type = obj.type; const type = obj.type;
if (type === 3) return obj.textColor || obj.color || "#000000"; if (type === PdfAnnotationSubtype.FREETEXT)
if (type !== undefined && [4, 5, 6, 7, 8].includes(type)) return obj.textColor || obj.color || "#000000";
if (
type !== undefined &&
STROKE_COLOR_SUBTYPES.includes(type as PdfAnnotationSubtype)
)
return obj.strokeColor || obj.color || "#000000"; return obj.strokeColor || obj.color || "#000000";
return obj.color || obj.strokeColor || "#000000"; return obj.color || obj.strokeColor || "#000000";
})(); })();
@@ -247,17 +296,23 @@ export function useAnnotationMenuHandlers({
patch.contents = obj?.contents ?? ""; patch.contents = obj?.contents ?? "";
} else { } else {
patch.color = color; patch.color = color;
if (type !== undefined && [9, 10, 11, 12].includes(type)) { if (
type !== undefined &&
TEXT_MARKUP_SUBTYPES.includes(type as PdfAnnotationSubtype)
) {
patch.strokeColor = color; patch.strokeColor = color;
patch.fillColor = color; patch.fillColor = color;
patch.opacity = obj?.opacity ?? 1; patch.opacity = obj?.opacity ?? 1;
} }
if (type !== undefined && [4, 8].includes(type)) { if (
type !== undefined &&
LINE_SUBTYPES.includes(type as PdfAnnotationSubtype)
) {
patch.strokeColor = color; patch.strokeColor = color;
patch.strokeWidth = obj?.strokeWidth ?? obj?.lineWidth ?? 2; patch.strokeWidth = obj?.strokeWidth ?? obj?.lineWidth ?? 2;
patch.lineWidth = obj?.lineWidth ?? obj?.strokeWidth ?? 2; patch.lineWidth = obj?.lineWidth ?? obj?.strokeWidth ?? 2;
} }
if (type === 15) { if (type === PdfAnnotationSubtype.INK) {
patch.strokeColor = color; patch.strokeColor = color;
patch.strokeWidth = obj?.strokeWidth ?? obj?.thickness ?? 2; patch.strokeWidth = obj?.strokeWidth ?? obj?.thickness ?? 2;
patch.opacity = obj?.opacity ?? 1; patch.opacity = obj?.opacity ?? 1;
@@ -1,8 +1,15 @@
import React, { createContext, useContext, ReactNode, useRef } from "react"; import React, { createContext, useContext, ReactNode, useRef } from "react";
import type { AnnotationAPI } from "@app/components/viewer/viewerTypes"; import type {
AnnotationAPI,
AnnotationToolId,
} from "@app/components/viewer/viewerTypes";
interface AnnotationContextValue { interface AnnotationContextValue {
annotationApiRef: React.RefObject<AnnotationAPI | null>; annotationApiRef: React.RefObject<AnnotationAPI | null>;
/** Ref to the panel-level activateAnnotationTool function — updates React state so buttons highlight correctly. Populated by Annotate.tsx. */
activateAnnotationToolRef: React.RefObject<
((toolId: AnnotationToolId) => void) | null
>;
} }
const AnnotationContext = createContext<AnnotationContextValue | undefined>( const AnnotationContext = createContext<AnnotationContextValue | undefined>(
@@ -13,9 +20,13 @@ export const AnnotationProvider: React.FC<{ children: ReactNode }> = ({
children, children,
}) => { }) => {
const annotationApiRef = useRef<AnnotationAPI>(null); const annotationApiRef = useRef<AnnotationAPI>(null);
const activateAnnotationToolRef = useRef<
((toolId: AnnotationToolId) => void) | null
>(null);
const value: AnnotationContextValue = { const value: AnnotationContextValue = {
annotationApiRef, annotationApiRef,
activateAnnotationToolRef,
}; };
return ( return (
+5
View File
@@ -6,6 +6,7 @@ import { useNavigation } from "@app/contexts/NavigationContext";
import { useFileSelection } from "@app/contexts/FileContext"; import { useFileSelection } from "@app/contexts/FileContext";
import { BaseToolProps } from "@app/types/tool"; import { BaseToolProps } from "@app/types/tool";
import { useSignature } from "@app/contexts/SignatureContext"; import { useSignature } from "@app/contexts/SignatureContext";
import { useAnnotation as useAnnotationContext } from "@app/contexts/AnnotationContext";
import { ViewerContext, useViewer } from "@app/contexts/ViewerContext"; import { ViewerContext, useViewer } from "@app/contexts/ViewerContext";
import type { import type {
AnnotationToolId, AnnotationToolId,
@@ -85,6 +86,7 @@ const Annotate = (_props: BaseToolProps) => {
placementPreviewSize, placementPreviewSize,
setPlacementPreviewSize, setPlacementPreviewSize,
} = useSignature(); } = useSignature();
const { activateAnnotationToolRef } = useAnnotationContext();
const viewerContext = useContext(ViewerContext); const viewerContext = useContext(ViewerContext);
const viewerContextRef = useRef(viewerContext); const viewerContextRef = useRef(viewerContext);
useEffect(() => { useEffect(() => {
@@ -375,6 +377,9 @@ const Annotate = (_props: BaseToolProps) => {
}, 300); }, 300);
}; };
// Keep ref in sync so external callers (e.g. CommentsSidebar) get the latest closure
activateAnnotationToolRef.current = activateAnnotationTool;
useEffect(() => { useEffect(() => {
// push style updates to EmbedPDF when sliders/colors change // push style updates to EmbedPDF when sliders/colors change
if (activeTool === "stamp") { if (activeTool === "stamp") {