feat: implement PDF toolbar, viewer components, and redaction support layer

This commit is contained in:
azeeee05
2026-06-30 11:32:15 +05:30
parent 8d4fa109ff
commit afa8c0a702
4 changed files with 79 additions and 14 deletions
+27 -10
View File
@@ -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([])}
/>
<div className="relative min-h-0 flex-1">
@@ -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}
+20 -1
View File
@@ -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<ToolId, { label: string; icon: React.ReactNode }> = {
@@ -54,6 +57,7 @@ const Divider = () => <div className="mx-1 h-5 w-px shrink-0 bg-[#ebedf0]" />;
export const Toolbar: React.FC<ToolbarProps> = ({
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<ToolbarProps> = ({
</>
)}
{activeTool === 'redact' && <Hint tone="warn"> Drag a box to permanently remove content underneath.</Hint>}
{activeTool === 'redact' && (
<>
{pendingRedactionCount > 0 ? (
<>
<CustomButton variant="primary" size="sm" onClick={onApplyRedactions}>
Apply {pendingRedactionCount} Redaction{pendingRedactionCount > 1 ? 's' : ''}
</CustomButton>
<CustomButton variant="outline" size="sm" onClick={onClearRedactions}>
Clear
</CustomButton>
</>
) : (
<Hint tone="warn"> Drag a box to permanently remove content underneath.</Hint>
)}
</>
)}
</div>
</div>
);
+9 -3
View File
@@ -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<PDFViewerRef, PDFViewerProps>(({
onAnnotationUpdate,
onAnnotationClick,
onPageVisible,
onRedactArea,
onMarkRedaction,
pendingRedactions = [],
onRemoveRedaction,
onPlaceText,
onEditText,
onReflowParagraph,
@@ -469,7 +473,9 @@ export const PDFViewer = React.forwardRef<PDFViewerRef, PDFViewerProps>(({
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}
/>
)}
+23
View File
@@ -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<RedactionLayerProps> = ({
width,
height,
onRedactionSelected,
pendingRedactions = [],
onRemoveRedaction,
}) => {
const [dragStart, setDragStart] = useState<Point | null>(null);
const [redactBox, setRedactBox] = useState<Rect | null>(null);
@@ -86,6 +90,25 @@ export const RedactionLayer: React.FC<RedactionLayerProps> = ({
}}
/>
)}
{pendingRedactions.map((redaction) => (
<div
key={redaction.id}
className="group absolute"
style={{
left: `${redaction.bounds.x}px`,
top: `${redaction.bounds.y}px`,
width: `${redaction.bounds.width}px`,
height: `${redaction.bounds.height}px`,
border: '2px solid #ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.2)',
zIndex: 30,
cursor: 'pointer',
}}
>
<div className="absolute inset-0 hidden bg-black group-hover:block" title="Click to remove" onClick={(e) => { e.stopPropagation(); onRemoveRedaction?.(redaction.id); }} />
</div>
))}
</div>
);
};