From afa8c0a7025bfa07e587d87d0d3cb31f8d4b02b6 Mon Sep 17 00:00:00 2001 From: azeeee05 Date: Tue, 30 Jun 2026 11:32:15 +0530 Subject: [PATCH] feat: implement PDF toolbar, viewer components, and redaction support layer --- frontend/src/App.tsx | 37 +++++++++++++++++++------- frontend/src/components/Toolbar.tsx | 21 ++++++++++++++- frontend/src/viewer/PDFViewer.tsx | 12 ++++++--- frontend/src/viewer/RedactionLayer.tsx | 23 ++++++++++++++++ 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ec46732..e820d89 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -370,19 +370,31 @@ function App() { setCurrentPage(to); }; - const handleRedactArea = (pageIndex: number, bounds: Rect) => { + const [pendingRedactions, setPendingRedactions] = useState<{ id: string, pageIndex: number, bounds: Rect }[]>([]); + + const handleMarkRedaction = (pageIndex: number, bounds: Rect) => { + setPendingRedactions(prev => [...prev, { id: rid('redmark'), pageIndex, bounds }]); + }; + + const handleApplyRedactions = () => { if (!activeDoc) return; if (!can('canModify')) { denyToast('Redaction'); return; } - const pdf = viewportRectToPdf(bounds, zoom, pageHeightPts(pageIndex)); + if (pendingRedactions.length === 0) return; + setConfirmState({ - title: 'Redact area?', - message: 'All text, images, and vectors underneath will be permanently removed from the file. This cannot be undone after export.', - confirmLabel: 'Redact', danger: true, + title: 'Apply Redactions?', + message: 'All text, images, and vectors underneath will be permanently removed from the file. This cannot be undone.', + confirmLabel: 'Apply', danger: true, onConfirm: () => { - applyOps([{ - id: rid('redact'), type: 'redaction', pageIndex, - data: { x: pdf.x, y: pdf.y, width: pdf.width, height: pdf.height, fillColor: '#ffffff' }, - }], 'Area redacted'); + const ops = pendingRedactions.map(mark => { + const pdf = viewportRectToPdf(mark.bounds, zoom, pageHeightPts(mark.pageIndex)); + return { + id: mark.id, type: 'redaction' as const, pageIndex: mark.pageIndex, + data: { x: pdf.x, y: pdf.y, width: pdf.width, height: pdf.height, fillColor: '#000000' }, + }; + }); + applyOps(ops, 'Redactions applied'); + setPendingRedactions([]); setActiveTool('select'); }, }); @@ -543,6 +555,9 @@ function App() { hasSignature={!!pendingSignature} activeStamp={activeStamp?.label ?? null} onSelectStamp={(label, color) => setActiveStamp({ label, color })} + pendingRedactionCount={pendingRedactions.length} + onApplyRedactions={handleApplyRedactions} + onClearRedactions={() => setPendingRedactions([])} />
@@ -585,7 +600,9 @@ function App() { if (!isInspectorOpen) setIsInspectorOpen(true); }} onPageVisible={setCurrentPage} - onRedactArea={handleRedactArea} + onMarkRedaction={handleMarkRedaction} + pendingRedactions={pendingRedactions} + onRemoveRedaction={(id) => setPendingRedactions(p => p.filter(x => x.id !== id))} onPlaceText={handlePlaceText} onEditText={handleEditText} onReflowParagraph={handleReflowParagraph} diff --git a/frontend/src/components/Toolbar.tsx b/frontend/src/components/Toolbar.tsx index 127bd26..00107fe 100644 --- a/frontend/src/components/Toolbar.tsx +++ b/frontend/src/components/Toolbar.tsx @@ -17,6 +17,9 @@ interface ToolbarProps { hasSignature: boolean; activeStamp: string | null; onSelectStamp: (label: string, color: string) => void; + pendingRedactionCount?: number; + onApplyRedactions?: () => void; + onClearRedactions?: () => void; } const TOOL_META: Record = { @@ -54,6 +57,7 @@ const Divider = () =>
; export const Toolbar: React.FC = ({ activeTool, settings, onSettingsChange, onOpenSignature, hasSignature, activeStamp, onSelectStamp, + pendingRedactionCount = 0, onApplyRedactions, onClearRedactions, }) => { const meta = TOOL_META[activeTool]; const isRedact = activeTool === 'redact'; @@ -161,7 +165,22 @@ export const Toolbar: React.FC = ({ )} - {activeTool === 'redact' && ⚠ Drag a box to permanently remove content underneath.} + {activeTool === 'redact' && ( + <> + {pendingRedactionCount > 0 ? ( + <> + + Apply {pendingRedactionCount} Redaction{pendingRedactionCount > 1 ? 's' : ''} + + + Clear + + + ) : ( + ⚠ Drag a box to permanently remove content underneath. + )} + + )}
); diff --git a/frontend/src/viewer/PDFViewer.tsx b/frontend/src/viewer/PDFViewer.tsx index a8a835e..81d00ea 100644 --- a/frontend/src/viewer/PDFViewer.tsx +++ b/frontend/src/viewer/PDFViewer.tsx @@ -36,7 +36,9 @@ interface PDFViewerProps { onAnnotationUpdate?: (anno: Annotation) => void; onAnnotationClick?: (anno: Annotation) => void; onPageVisible?: (pageIndex: number) => void; - onRedactArea?: (pageIndex: number, bounds: Rect) => void; + onMarkRedaction?: (pageIndex: number, bounds: Rect) => void; + pendingRedactions?: { id: string, pageIndex: number, bounds: Rect }[]; + onRemoveRedaction?: (id: string) => void; onPlaceText?: (pageIndex: number, rectPts: Rect, text: string) => void; onEditText?: (pageIndex: number, run: EditableRun, newText: string, disableJustify?: boolean) => void; onReflowParagraph?: (pageIndex: number, payload: ReflowParagraphPayload) => void; @@ -80,7 +82,9 @@ export const PDFViewer = React.forwardRef(({ onAnnotationUpdate, onAnnotationClick, onPageVisible, - onRedactArea, + onMarkRedaction, + pendingRedactions = [], + onRemoveRedaction, onPlaceText, onEditText, onReflowParagraph, @@ -469,7 +473,9 @@ export const PDFViewer = React.forwardRef(({ pageIndex={page.index} width={page.width} height={page.height} - onRedactionSelected={(bounds) => onRedactArea?.(page.index, bounds)} + onRedactionSelected={(bounds) => onMarkRedaction?.(page.index, bounds)} + pendingRedactions={pendingRedactions.filter(r => r.pageIndex === page.index)} + onRemoveRedaction={onRemoveRedaction} /> )} diff --git a/frontend/src/viewer/RedactionLayer.tsx b/frontend/src/viewer/RedactionLayer.tsx index 343c8af..3817884 100644 --- a/frontend/src/viewer/RedactionLayer.tsx +++ b/frontend/src/viewer/RedactionLayer.tsx @@ -6,12 +6,16 @@ interface RedactionLayerProps { width: number; height: number; onRedactionSelected: (bounds: Rect) => void; + pendingRedactions?: { id: string, bounds: Rect }[]; + onRemoveRedaction?: (id: string) => void; } export const RedactionLayer: React.FC = ({ width, height, onRedactionSelected, + pendingRedactions = [], + onRemoveRedaction, }) => { const [dragStart, setDragStart] = useState(null); const [redactBox, setRedactBox] = useState(null); @@ -86,6 +90,25 @@ export const RedactionLayer: React.FC = ({ }} /> )} + + {pendingRedactions.map((redaction) => ( +
+
{ e.stopPropagation(); onRemoveRedaction?.(redaction.id); }} /> +
+ ))}
); };