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">
{t( <LocalIcon
"viewer.comments.hint", icon="comment"
"Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.", width="2rem"
)} height="2rem"
</Text> style={{ color: "var(--mantine-color-dimmed)" }}
/>
<Text size="sm" c="dimmed" ta="center">
{t(
"viewer.comments.hint",
"Place comments with the Comment, Insert Text, or Replace Text tools. They will appear here by page.",
)}
</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") {