From 282f18705d74a2e7cf73469e69a3335b61257fd4 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Wed, 17 Jun 2026 16:44:02 +0100 Subject: [PATCH] draft: Make Tool Steps Seperate Pages --- .../public/locales/en-US/translation.toml | 9 + .../src/core/components/tools/ToolPanel.tsx | 13 +- .../tools/redact/ManualRedactionControls.tsx | 87 +++++- .../tools/shared/ReviewToolStep.tsx | 2 +- .../tools/shared/ToolStepWizard.css | 126 ++++++++ .../tools/shared/ToolStepWizard.tsx | 281 ++++++++++++++++++ .../tools/shared/WizardFilesStep.css | 42 +++ .../tools/shared/WizardFilesStep.tsx | 104 +++++++ .../tools/shared/createToolFlow.tsx | 200 ++++++++++--- .../components/viewer/RedactionAPIBridge.tsx | 6 + .../src/core/contexts/RedactionContext.tsx | 12 +- 11 files changed, 824 insertions(+), 58 deletions(-) create mode 100644 frontend/editor/src/core/components/tools/shared/ToolStepWizard.css create mode 100644 frontend/editor/src/core/components/tools/shared/ToolStepWizard.tsx create mode 100644 frontend/editor/src/core/components/tools/shared/WizardFilesStep.css create mode 100644 frontend/editor/src/core/components/tools/shared/WizardFilesStep.tsx diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 5b22c30644..c689e24f9e 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -4,6 +4,10 @@ apply = "Apply" applyAndContinue = "Save & Leave" areYouSure = "Are you sure you want to leave?" back = "Back" +wizardContinue = "Continue" +wizardProgress = "Step progress" +wizardStepProgress = "Step {{current}} of {{total}}" +wizardCompleteStep = "Complete this step to continue." black = "Black" blue = "Blue" cancel = "Cancel" @@ -3871,6 +3875,9 @@ title = "File to PDF" [fileUpload] dropFilesHere = "Drop files here or click the upload button" +dropTitle = "Drop your file here" +dropSubtitle = "or pick one from your computer" +addAtLeast = "Add at least {{count}} files" dropFilesHereOpen = "Drop files here or click the open button" filesAvailable = "files available" loadFromStorage = "Load from Storage" @@ -6242,6 +6249,8 @@ applyWarning = "⚠️ Permanent application, cannot be undone and the data unde colorLabel = "Redaction Color" controlsTitle = "Manual Redaction Controls" instructions = "Select text or draw areas on the PDF to mark content for redaction." +markArea = "Mark Area" +markText = "Mark Text" title = "Redaction Tools" [redact.modeSelector] diff --git a/frontend/editor/src/core/components/tools/ToolPanel.tsx b/frontend/editor/src/core/components/tools/ToolPanel.tsx index 2d6006a8eb..12f4d881a9 100644 --- a/frontend/editor/src/core/components/tools/ToolPanel.tsx +++ b/frontend/editor/src/core/components/tools/ToolPanel.tsx @@ -66,7 +66,18 @@ export default function ToolPanel({ ) : (
- + {selectedToolKey ? ( { if ( disabled || @@ -77,7 +80,7 @@ export default function ManualRedactionControls({ // Small delay to avoid racing with EmbedPDF's own state updates const timer = setTimeout(() => { if (!isSavingRef.current) { - activateManualRedact(); + activateTextSelection(); } }, 50); return () => clearTimeout(timer); @@ -90,7 +93,7 @@ export default function ManualRedactionControls({ showNavigationWarning, setAnnotationMode, signatureApiRef, - activateManualRedact, + activateTextSelection, ]); // Reset redaction tool when switching between files @@ -123,6 +126,28 @@ export default function ManualRedactionControls({ const isApiReady = isBridgeReady; + // Which manual-redaction sub-mode is active (drives the tool button highlight). + const isSelectionActive = activeType === "redactSelection"; + const isMarqueeActive = activeType === "marqueeRedact"; + + // Switch to a redaction sub-mode, first stepping out of annotation mode/tools. + const switchMode = useCallback( + (activate: () => void) => { + if (isAnnotationMode) { + setAnnotationMode(false); + if (signatureApiRef?.current) { + try { + signatureApiRef.current.deactivateTools(); + } catch (error) { + console.log("Unable to deactivate annotation tools:", error); + } + } + } + activate(); + }, + [isAnnotationMode, setAnnotationMode, signatureApiRef], + ); + return ( <> @@ -138,6 +163,54 @@ export default function ManualRedactionControls({ )} + + + + + + { onCollapsedClick?: () => void; } -function ReviewStepContent({ +export function ReviewStepContent({ operation, onFileClick, onUndo, diff --git a/frontend/editor/src/core/components/tools/shared/ToolStepWizard.css b/frontend/editor/src/core/components/tools/shared/ToolStepWizard.css new file mode 100644 index 0000000000..3f1c075473 --- /dev/null +++ b/frontend/editor/src/core/components/tools/shared/ToolStepWizard.css @@ -0,0 +1,126 @@ +/* Slide-based tool wizard: a calm segmented progress bar, one step per slide + with centered content, and big Back / Continue CTAs pinned to the bottom. */ + +.tool-step-wizard { + /* Fill the panel height (the parent ScrollArea content is a flex column with + min-height:100%). flex-basis:0 makes this resolve to exactly the viewport + height so the body can scroll internally and the CTAs stay pinned. */ + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; + width: 100%; +} + +.tool-step-wizard__title { + flex-shrink: 0; + padding: 0.5rem 0.75rem 0; +} + +/* ---- Progress bar ---- */ +.tool-step-wizard__progress { + display: flex; + gap: 6px; + align-items: center; + padding: 0.85rem 1rem 0.45rem; + flex-shrink: 0; +} + +.tool-step-wizard__segment { + all: unset; + flex: 1 1 0; + height: 4px; + border-radius: 9999px; + background: var(--onboarding-step-inactive); + position: relative; + overflow: hidden; + cursor: default; + box-sizing: border-box; +} + +.tool-step-wizard__segment[data-jumpable] { + cursor: pointer; +} + +.tool-step-wizard__segment-fill { + position: absolute; + inset: 0; + border-radius: inherit; + background: var(--mantine-color-blue-filled); + transform: scaleX(0); + transform-origin: left center; + transition: transform 0.3s ease; +} + +.tool-step-wizard__segment[data-filled] .tool-step-wizard__segment-fill { + transform: scaleX(1); +} + +.tool-step-wizard__caption { + flex-shrink: 0; + padding: 0 1rem 0.25rem; +} + +/* ---- Slide body: top-aligned content that scrolls when tall ---- */ +.tool-step-wizard__body { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + /* Horizontal padding lives on the slide + CTA buttons so the CTA's top + border can span edge-to-edge. */ + padding-top: 0.75rem; +} + +.tool-step-wizard__slide { + width: 100%; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 0.85rem; + padding: 0 1rem; + animation: wizard-slide-in 0.2s ease; +} + +@keyframes wizard-slide-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.tool-step-wizard__slide-content { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +/* ---- CTAs: sit right after the content, stick to the bottom on overflow ---- */ +.tool-step-wizard__cta { + position: sticky; + bottom: 0; + /* Flex column so each button stretches to the container width minus its own + horizontal margin (mx="md") — never width:100% + margin, which overflows. */ + display: flex; + flex-direction: column; + margin-top: 0.75rem; + padding-bottom: 0.85rem; + border-top: 1px solid var(--border-subtle); + background: var(--bg-toolbar); + z-index: 1; +} + +.tool-step-wizard__cta-btn { + min-height: 2.85rem !important; +} + +@media (prefers-reduced-motion: reduce) { + .tool-step-wizard__segment-fill, + .tool-step-wizard__slide { + transition: none !important; + animation: none !important; + } +} diff --git a/frontend/editor/src/core/components/tools/shared/ToolStepWizard.tsx b/frontend/editor/src/core/components/tools/shared/ToolStepWizard.tsx new file mode 100644 index 0000000000..c74f0e6bcf --- /dev/null +++ b/frontend/editor/src/core/components/tools/shared/ToolStepWizard.tsx @@ -0,0 +1,281 @@ +import React, { useEffect, useState } from "react"; +import { Box, Button, Flex, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { Tooltip } from "@app/components/shared/Tooltip"; +import { + ToolWorkflowTitle, + ToolWorkflowTitleProps, +} from "@app/components/tools/shared/ToolWorkflowTitle"; +import type { TooltipTip } from "@app/types/tips"; +import "@app/components/tools/shared/ToolStepWizard.css"; + +export interface WizardSlide { + key: string; + title: string; + content: React.ReactNode; + /** Whether the user may advance past this slide (gates Continue + forward jumps). */ + isValid: boolean; + /** Tooltip shown on the Continue button when this slide blocks advancing. */ + blockedHint?: string; + /** + * Hide the bottom Continue button for this slide — used by the Files slide, + * whose own upload CTA advances the wizard once enough files are added. + */ + hideContinue?: boolean; + /** + * Invoked when the user navigates to this slide by clicking its progress-bar + * segment in terminal (results) mode — used to reset the results so the tool + * returns to editing this step. + */ + onActivate?: () => void; + tooltip?: { + content?: React.ReactNode; + tips?: TooltipTip[]; + header?: { + title: string; + logo?: React.ReactNode; + }; + }; +} + +export interface ToolStepWizardProps { + slides: WizardSlide[]; + title?: ToolWorkflowTitleProps; + /** Primary CTA on the final slide (the execute button). */ + executeSlot?: React.ReactNode; + /** Rendered just beneath the execute button on the final slide. */ + belowExecuteButton?: React.ReactNode; + /** Optional preview content shown on the final slide above the CTA. */ + preview?: React.ReactNode; + /** + * Terminal (results) mode: the flow is complete, so the wizard sits on the + * last slide with the whole progress bar filled and no Continue/Back CTAs (the + * review content carries its own undo/download). Clicking an earlier bar + * segment fires that slide's onActivate (reset) to return to editing it. + */ + terminal?: boolean; +} + +/** + * Renders a tool's steps as a slide wizard: a segmented progress bar at the top, + * one step per slide with centered content, and large Back / Continue CTAs at + * the bottom. The final slide swaps Continue for the tool's execute button. A + * single-step flow drops the progress bar / step counter / title and just shows + * the centered content and its primary action. + * + * Navigation is gated: Continue and forward jumps are blocked until the current + * slide is valid; Back (and clicking an earlier segment) is always allowed. + */ +export function ToolStepWizard({ + slides, + title, + executeSlot, + belowExecuteButton, + preview, + terminal = false, +}: ToolStepWizardProps) { + const { t } = useTranslation(); + const [activeKey, setActiveKey] = useState(() => slides[0]?.key); + + // Keep activeKey valid as the slide set changes (e.g. the Files slide drops + // out once a file is selected). Falls back to the first slide. + useEffect(() => { + if (slides.length > 0 && !slides.some((s) => s.key === activeKey)) { + setActiveKey(slides[0].key); + } + }, [slides, activeKey]); + + if (slides.length === 0) return null; + + // In terminal (results) mode the flow is complete: lock onto the last slide. + const rawIndex = slides.findIndex((s) => s.key === activeKey); + const currentIndex = terminal + ? slides.length - 1 + : rawIndex < 0 + ? 0 + : rawIndex; + const current = slides[currentIndex]; + const isLast = currentIndex === slides.length - 1; + const canAdvance = current.isValid; + const showBack = !terminal && currentIndex > 0; + // A single-step flow is just centered content + its action — no progress bar, + // step counter, or title (a lone full-width segment reads as an odd solid + // line). The chrome appears only with 2+ steps. + const showChrome = slides.length > 1; + const showContinue = !isLast && !current.hideContinue; + const hasCta = isLast + ? Boolean(executeSlot) || Boolean(belowExecuteButton) + : showContinue || showBack; + + // Forward jumps require every intervening slide to be valid; back is free. + // In terminal (results) mode every earlier step is reachable to go back and edit. + const canJumpTo = (target: number) => { + if (terminal) return target < currentIndex; + if (target === currentIndex) return false; + if (target < currentIndex) return true; + for (let i = currentIndex; i < target; i++) { + if (!slides[i].isValid) return false; + } + return true; + }; + + const goTo = (target: number) => { + if (target < 0 || target >= slides.length) return; + // In terminal mode, navigating back resets the results (returns to editing). + if (terminal) slides[target].onActivate?.(); + setActiveKey(slides[target].key); + }; + + return ( +
+ {title && ( +
+ +
+ )} + + {showChrome && ( +
+ {slides.map((slide, index) => { + const filled = index <= currentIndex; + const isCurrent = index === currentIndex; + const jumpable = canJumpTo(index); + return ( + + ); + })} +
+ )} + + {showChrome && ( + + {t("wizardStepProgress", "Step {{current}} of {{total}}", { + current: currentIndex + 1, + total: slides.length, + })} + + )} + +
+
+ {showChrome && + (current.tooltip ? ( + + + + {current.title} + + + + + ) : ( + + {current.title} + + ))} + +
+ {current.content} +
+ + {isLast && preview &&
{preview}
} +
+ + {hasCta && ( +
+ {isLast ? ( + (executeSlot ?? null) + ) : showContinue ? ( + !canAdvance && current.blockedHint ? ( + + + + + + ) : ( + + ) + ) : null} + {isLast && belowExecuteButton} + {showBack && ( + + )} +
+ )} +
+
+ ); +} + +export default ToolStepWizard; diff --git a/frontend/editor/src/core/components/tools/shared/WizardFilesStep.css b/frontend/editor/src/core/components/tools/shared/WizardFilesStep.css new file mode 100644 index 0000000000..40959b6eea --- /dev/null +++ b/frontend/editor/src/core/components/tools/shared/WizardFilesStep.css @@ -0,0 +1,42 @@ +/* Large, clean upload CTA used as the Files slide in the tool step wizard. */ + +.wizard-files-dropzone { + width: 100%; + border: 1.5px dashed var(--border-subtle, var(--mantine-color-default-border)); + border-radius: var(--mantine-radius-lg, 1rem); + background: transparent; + padding: 2.25rem 1.25rem; + transition: + border-color 0.2s ease, + background-color 0.2s ease; +} + +.wizard-files-dropzone:hover { + border-color: var(--mantine-color-blue-filled); + background: color-mix( + in srgb, + var(--mantine-color-blue-filled) 5%, + transparent + ); +} + +.wizard-files-dropzone[data-accept] { + border-color: var(--mantine-color-blue-filled); + background: var(--mantine-color-blue-light); +} + +.wizard-files-dropzone[data-reject] { + border-color: var(--mantine-color-red-filled); +} + +.wizard-files-dropzone__icon { + width: 3.5rem; + height: 3.5rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--mantine-color-blue-light); + color: var(--mantine-color-blue-filled); + margin-bottom: 0.25rem; +} diff --git a/frontend/editor/src/core/components/tools/shared/WizardFilesStep.tsx b/frontend/editor/src/core/components/tools/shared/WizardFilesStep.tsx new file mode 100644 index 0000000000..e1ae028fe2 --- /dev/null +++ b/frontend/editor/src/core/components/tools/shared/WizardFilesStep.tsx @@ -0,0 +1,104 @@ +import { Button, Group, Stack, Text } from "@mantine/core"; +import { Dropzone } from "@mantine/dropzone"; +import { useTranslation } from "react-i18next"; +import LocalIcon from "@app/components/shared/LocalIcon"; +import { useFileHandler } from "@app/hooks/useFileHandler"; +import { useFilesModalContext } from "@app/contexts/FilesModalContext"; +import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology"; +import { StirlingFile } from "@app/types/fileContext"; +import "@app/components/tools/shared/WizardFilesStep.css"; + +export interface WizardFilesStepProps { + selectedFiles: StirlingFile[]; + minFiles?: number; +} + +/** + * The Files slide of the tool step wizard: a large drag-and-drop upload CTA. + * Adding enough files auto-advances the wizard (the slide drops out), so this + * slide owns the upload action and has no Continue button of its own. + */ +export function WizardFilesStep({ + selectedFiles, + minFiles = 1, +}: WizardFilesStepProps) { + const { t } = useTranslation(); + const { addFiles } = useFileHandler(); + const { openFilesModal } = useFilesModalContext(); + const terminology = useFileActionTerminology(); + + const handleDrop = (files: File[]) => { + if (files.length > 0) void addFiles(files); + }; + + const handleUpload = () => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.accept = ".pdf,application/pdf"; + input.onchange = (event) => { + const files = Array.from((event.target as HTMLInputElement).files || []); + if (files.length > 0) void addFiles(files); + }; + input.click(); + }; + + const needsMore = minFiles > 1; + const count = selectedFiles.length; + + return ( + + +
+ +
+ + {t("fileUpload.dropTitle", "Drop your file here")} + + + {needsMore + ? t("fileUpload.addAtLeast", "Add at least {{count}} files", { + count: minFiles, + }) + : t("fileUpload.dropSubtitle", "or pick one from your computer")} + + {count > 0 && ( + + {t("filesSelected", "{{count}} files", { count })} + + )} + + + + +
+
+ ); +} + +export default WizardFilesStep; diff --git a/frontend/editor/src/core/components/tools/shared/createToolFlow.tsx b/frontend/editor/src/core/components/tools/shared/createToolFlow.tsx index aafa9b3ea1..e3ed429a7b 100644 --- a/frontend/editor/src/core/components/tools/shared/createToolFlow.tsx +++ b/frontend/editor/src/core/components/tools/shared/createToolFlow.tsx @@ -5,6 +5,12 @@ import { ToolStepProvider, } from "@app/components/tools/shared/ToolStep"; import { ScopedOperationButton } from "@app/components/tools/shared/ScopedOperationButton"; +import { + ToolStepWizard, + WizardSlide, +} from "@app/components/tools/shared/ToolStepWizard"; +import { WizardFilesStep } from "@app/components/tools/shared/WizardFilesStep"; +import { ReviewStepContent } from "@app/components/tools/shared/ReviewToolStep"; import { ToolOperationHook } from "@app/hooks/tools/shared/useToolOperation"; import { ToolWorkflowTitle, @@ -13,6 +19,7 @@ import { import { StirlingFile } from "@app/types/fileContext"; import type { TooltipTip } from "@app/types/tips"; import type { ExecuteDisabledReason } from "@app/hooks/tools/shared/toolOperationTypes"; +import i18n from "@app/i18n"; export interface FilesStepConfig { selectedFiles: StirlingFile[]; @@ -27,6 +34,12 @@ export interface MiddleStepConfig { isVisible?: boolean; isCollapsed?: boolean; onCollapsedClick?: () => void; + /** + * In the slide wizard, whether the user may advance past this step. Defaults + * to true (the step is always passable). Set false to block Continue and + * forward jumps until the step's inputs are valid. + */ + canContinue?: boolean; content: React.ReactNode; tooltip?: { content?: React.ReactNode; @@ -98,11 +111,141 @@ export interface ToolFlowConfig { export function createToolFlow( config: ToolFlowConfig, ) { - const steps = createToolSteps(); + // Execute button, shared by the wizard (final-slide CTA) and the accordion. + const renderExecuteButton = (): React.ReactNode => { + const eb = config.executeButton; + if (!eb || eb.isVisible === false) return null; + const hasFiles = (config.files.selectedFiles?.length ?? 0) > 0; + // Explicit disabledReason wins; otherwise derive from structured fields. + const effectiveDisabledReason: ExecuteDisabledReason = + eb.disabledReason !== undefined + ? eb.disabledReason + : eb.endpointEnabled === false + ? "endpointUnavailable" + : !hasFiles + ? "noFiles" + : eb.paramsValid === false + ? "invalidParams" + : null; + return ( + + ); + }; + // ---- Build the pre-execution slide list for the wizard ---- + const minFiles = config.files.minFiles ?? 1; + const filesCount = config.files.selectedFiles?.length ?? 0; + const filesSatisfied = filesCount >= minFiles; + const filesVisible = config.files.isVisible !== false; + // Mirror the accordion's "collapse the Files step once selected": the Files + // slide only appears while files are still needed. + const includeFilesSlide = filesVisible && !filesSatisfied; + const middleSteps = config.steps.filter((s) => s.isVisible !== false); + + const middleSlides: WizardSlide[] = middleSteps.map((step, index) => ({ + key: `step-${index}-${step.title}`, + title: step.title, + content: step.content, + tooltip: step.tooltip, + isValid: step.canContinue ?? true, + blockedHint: i18n.t( + "wizardCompleteStep", + "Complete this step to continue.", + ), + })); + + // Key by tool identity so switching tools resets the wizard, while + // adding/removing slides within a tool is handled by the wizard itself. + const wizardKey = `${config.executeButton?.text ?? ""}|${middleSteps + .map((s) => s.title) + .join(">")}`; + + // ---- Results view: a completed progress bar + the review content (no + // collapsed step sections). The review is the final, terminal slide. ---- + if (config.review.isVisible) { + // Clicking an earlier segment returns to editing that step: run the step's + // own reset (clears preview etc.) and clear results so we leave results mode. + const resultSlides: WizardSlide[] = [ + ...middleSlides.map((slide, index) => ({ + ...slide, + onActivate: () => { + middleSteps[index].onCollapsedClick?.(); + config.review.operation.resetResults(); + }, + })), + { + key: "__review__", + title: config.review.title, + content: ( + + ), + isValid: true, + }, + ]; + return ( + + ); + } + + // ---- Pre-execution flow ---- + const wizardSlides: WizardSlide[] = []; + if (includeFilesSlide) { + wizardSlides.push({ + key: "__files__", + title: i18n.t("files.title", "Files"), + content: ( + + ), + isValid: filesSatisfied, + // The upload CTA advances the wizard once enough files are added. + hideContinue: true, + }); + } + wizardSlides.push(...middleSlides); + + if (wizardSlides.length >= 1) { + return ( + 0 ? config.preview : undefined} + /> + ); + } + + // ---- Accordion fallback (rare zero-slide tool: file selected, no steps) ---- + const steps = createToolSteps(); return ( - {/* */} {config.title && } @@ -131,50 +274,19 @@ export function createToolFlow( {/* Preview (outside steps, above execute button). Hide when review is visible or when no files are selected. */} - {!config.review.isVisible && - (config.files.selectedFiles?.length ?? 0) > 0 && - config.preview} + {!config.review.isVisible && filesCount > 0 && config.preview} {/* Execute Button */} - {config.executeButton && - config.executeButton.isVisible !== false && - (() => { - const eb = config.executeButton; - const hasFiles = (config.files.selectedFiles?.length ?? 0) > 0; - // Compute the disabled reason from structured fields; explicit disabledReason wins if set. - const effectiveDisabledReason: ExecuteDisabledReason = - eb.disabledReason !== undefined - ? eb.disabledReason - : eb.endpointEnabled === false - ? "endpointUnavailable" - : !hasFiles - ? "noFiles" - : eb.paramsValid === false - ? "invalidParams" - : null; - return ( - <> - - {config.belowExecuteButton} - - ); - })()} + {(() => { + const button = renderExecuteButton(); + if (!button) return null; + return ( + <> + {button} + {config.belowExecuteButton} + + ); + })()} {/* Review Step */} {steps.createReviewStep({ diff --git a/frontend/editor/src/core/components/viewer/RedactionAPIBridge.tsx b/frontend/editor/src/core/components/viewer/RedactionAPIBridge.tsx index c860e074d0..13b39448e6 100644 --- a/frontend/editor/src/core/components/viewer/RedactionAPIBridge.tsx +++ b/frontend/editor/src/core/components/viewer/RedactionAPIBridge.tsx @@ -80,6 +80,12 @@ function RedactionAPIBridgeInner({ documentId }: { documentId: string }) { enableRedact: () => { redactionProvides?.enableRedact(); }, + enableRedactSelection: () => { + redactionProvides?.enableRedactSelection(); + }, + enableMarqueeRedact: () => { + redactionProvides?.enableMarqueeRedact(); + }, isRedactActive: () => { return redactionProvides?.isRedactActive() ?? false; }, diff --git a/frontend/editor/src/core/contexts/RedactionContext.tsx b/frontend/editor/src/core/contexts/RedactionContext.tsx index 0391642cda..3ba4096b98 100644 --- a/frontend/editor/src/core/contexts/RedactionContext.tsx +++ b/frontend/editor/src/core/contexts/RedactionContext.tsx @@ -17,6 +17,8 @@ import { RedactionMode } from "@embedpdf/plugin-redaction"; export interface RedactionAPI { toggleRedact: () => void; enableRedact: () => void; + enableRedactSelection: () => void; + enableMarqueeRedact: () => void; isRedactActive: () => boolean; endRedact: () => void; // Common methods @@ -211,19 +213,19 @@ export const RedactionProvider: React.FC<{ children: ReactNode }> = ({ } }, []); - // Legacy UI actions for backwards compatibility - // In v2.5.0, both text selection and marquee use the same unified mode - // These just activate the unified redact mode and set the active type for UI state + // Activate a specific manual-redaction sub-mode. The plugin (v2.14.1) has + // dedicated text-selection and marquee modes; the optimistic setActiveType is + // reconciled by RedactionAPIBridge syncing the real state from EmbedPDF. const activateTextSelection = useCallback(() => { if (redactionApiRef.current) { - redactionApiRef.current.enableRedact(); + redactionApiRef.current.enableRedactSelection(); setActiveType("redactSelection" as RedactionMode); } }, [setActiveType]); const activateMarquee = useCallback(() => { if (redactionApiRef.current) { - redactionApiRef.current.enableRedact(); + redactionApiRef.current.enableMarqueeRedact(); setActiveType("marqueeRedact" as RedactionMode); } }, [setActiveType]);