diff --git a/package-lock.json b/package-lock.json index cdfd30c..be5c2a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3187,6 +3187,7 @@ } } }, + "node_modules/@reown/appkit-pay": { "version": "1.7.8", "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz", @@ -3544,6 +3545,7 @@ } } }, + "node_modules/@reown/appkit-wallet": { "version": "1.7.8", "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz", @@ -3853,6 +3855,7 @@ } } }, + "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", @@ -6523,6 +6526,7 @@ } } }, + "node_modules/@walletconnect/window-getters": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", diff --git a/src/pages/EditPdf/EditPdf.jsx b/src/pages/EditPdf/EditPdf.jsx index 4c72d7e..66053b7 100644 --- a/src/pages/EditPdf/EditPdf.jsx +++ b/src/pages/EditPdf/EditPdf.jsx @@ -16,15 +16,17 @@ import { FREE_LIMITS, mbToBytes } from "../../config/limits"; // ── constants ──────────────────────────────────────────────────────────────── const RENDER_SCALE = 1.5; const ANN_TOOLS = [ - { id: "draw", label: "Draw" }, - { id: "text", label: "Text" }, - { id: "highlight", label: "Highlight" }, - { id: "rect", label: "Rectangle" }, - { id: "eraser", label: "Eraser" }, + { id: "draw", label: "Draw", icon: Pencil }, + { id: "text", label: "Add Text Box", icon: Type }, + { id: "highlight", label: "Highlight", icon: Highlighter }, + { id: "rect", label: "Rectangle", icon: Square }, + { id: "eraser", label: "Eraser", icon: Eraser }, ]; const COLORS = ["#ef4444","#f97316","#eab308","#22c55e","#3b82f6","#a855f7","#ec4899","#000000","#ffffff"]; -const HINTS = { draw:"Click and drag freely", text:"Click to place a text box", highlight:"Drag to highlight an area", rect:"Drag to draw a rectangle", eraser:"Click any annotation to remove it" }; +const HINTS = { draw:"Click and drag freely", text:"Click anywhere on the page to place a new text box", highlight:"Drag to highlight an area", rect:"Drag to draw a rectangle", eraser:"Click any annotation to remove it" }; const CURSOR = { draw:"crosshair", text:"text", highlight:"crosshair", rect:"crosshair", eraser:"cell" }; +const TEXTBOX_PADDING_X = 4; +const TEXTBOX_PADDING_Y = 2; // ── canvas helpers ──────────────────────────────────────────────────────────── function drawAnn(ctx, ann) { @@ -48,9 +50,7 @@ function drawAnn(ctx, ann) { ctx.strokeRect(x, y, Math.abs((ann.x2??ann.x)-ann.x), Math.abs((ann.y2??ann.y)-ann.y)); break; } case "text": - ctx.globalAlpha = ann.opacity ?? 1; - ctx.font = `${ann.fontSize??18}px sans-serif`; - ctx.fillText(ann.text, ann.x, ann.y); break; + break; } ctx.restore(); } @@ -59,11 +59,14 @@ function hitTest(ann, x, y) { const R = Math.max(10, (ann.strokeWidth??2)*3); switch (ann.type) { case "draw": return ann.points?.some(p => Math.hypot(p.x-x, p.y-y) < R); - case "text": { const fs = ann.fontSize??18; return x>=ann.x-4 && x<=ann.x+ann.text.length*fs*0.6 && y>=ann.y-fs && y<=ann.y+4; } + case "text": { const fs = ann.fontSize??18; return x>=ann.x-4 && x<=ann.x+ann.text.length*fs*0.6 && y>=ann.y-4 && y<=ann.y+fs+4; } default: return x>=Math.min(ann.x,ann.x2??ann.x)-4&&x<=Math.max(ann.x,ann.x2??ann.x)+4&&y>=Math.min(ann.y,ann.y2??ann.y)-4&&y<=Math.max(ann.y,ann.y2??ann.y)+4; } } function getPos(e, el) { const r = el.getBoundingClientRect(); return { x: e.clientX-r.left, y: e.clientY-r.top }; } +function isFormTarget(target) { + return target instanceof HTMLElement && ["TEXTAREA", "INPUT", "BUTTON"].includes(target.tagName); +} // ── component ───────────────────────────────────────────────────────────────── export function EditPdf() { @@ -93,6 +96,8 @@ export function EditPdf() { const canvasRefs = useRef({}); const drawing = useRef(null); + const draggingText = useRef(null); + const skipTextPlacement = useRef(false); const { isPremium, isWalletConnected: isConn, hasReachedGlobalLimit, incrementUsage } = useSubscription(); const LIMIT_MB = FREE_LIMITS.editPdf.maxFileSizeMb; @@ -169,15 +174,45 @@ export function EditPdf() { }); // ── 2. Draw annotation strokes on top ──────────────────────────────────── - annotations.filter(a => a.pageIndex === pi).forEach(a => drawAnn(ctx, a)); + annotations.filter(a => a.pageIndex === pi && a.type !== "text").forEach(a => drawAnn(ctx, a)); if (inProgress) drawAnn(ctx, inProgress); } // ── annotation mouse handlers ──────────────────────────────────────────────── + function openTextBox(pi, pos) { + setTextBox({ + pageIndex: pi, + x: Math.max(8, pos.x), + y: Math.max(8, pos.y), + value: "", + }); + } + function removeAnnotation(id) { + setAnnotations(prev => prev.filter(ann => ann.id !== id)); + } + function startTextDrag(e, ann) { + if (mode !== "annotate") return; + if (tool === "eraser") { + e.stopPropagation(); + removeAnnotation(ann.id); + return; + } + if (tool !== "text") return; + const pos = getPos(e, canvasRefs.current[ann.pageIndex]); + draggingText.current = { + id: ann.id, + pageIndex: ann.pageIndex, + offsetX: pos.x - ann.x, + offsetY: pos.y - ann.y, + }; + skipTextPlacement.current = true; + e.stopPropagation(); + } + function onMouseDown(e, pi) { if (mode !== "annotate") return; const pos = getPos(e, canvasRefs.current[pi]); - if (tool === "text") { setTextBox({ pageIndex: pi, x: pos.x, y: pos.y, value: "" }); return; } + if (tool === "text") return; if (tool === "eraser") { setAnnotations(prev => { const idx = [...prev].reverse().findIndex(a => a.pageIndex === pi && hitTest(a, pos.x, pos.y)); @@ -186,7 +221,26 @@ export function EditPdf() { } drawing.current = { pageIndex: pi, type: tool, color, strokeWidth: stroke, opacity, x: pos.x, y: pos.y, x2: pos.x, y2: pos.y, points: [pos] }; } + function onPageClick(e, pi) { + if (skipTextPlacement.current) { + skipTextPlacement.current = false; + return; + } + if (mode !== "annotate" || tool !== "text" || textBox || isFormTarget(e.target)) return; + const holder = e.currentTarget; + openTextBox(pi, getPos(e, holder)); + } function onMouseMove(e, pi) { + if (draggingText.current?.pageIndex === pi) { + const pos = getPos(e, canvasRefs.current[pi]); + const { id, offsetX, offsetY } = draggingText.current; + setAnnotations(prev => prev.map(ann => ( + ann.id === id + ? { ...ann, x: Math.max(8, pos.x - offsetX), y: Math.max(8, pos.y - offsetY) } + : ann + ))); + return; + } if (!drawing.current || drawing.current.pageIndex !== pi) return; const pos = getPos(e, canvasRefs.current[pi]); drawing.current.x2 = pos.x; drawing.current.y2 = pos.y; @@ -194,15 +248,35 @@ export function EditPdf() { redraw(pi, drawing.current); } function onMouseUp(e, pi) { + if (draggingText.current?.pageIndex === pi) { + draggingText.current = null; + return; + } if (!drawing.current || drawing.current.pageIndex !== pi) return; const d = drawing.current; drawing.current = null; const ok = d.type === "draw" ? d.points.length >= 2 : Math.abs(d.x2-d.x) > 3 || Math.abs(d.y2-d.y) > 3; if (ok) setAnnotations(prev => [...prev, { ...d, id: Date.now() }]); else redraw(pi, null); } - function commitTextBox() { - if (!textBox) return; - if (textBox.value.trim()) setAnnotations(prev => [...prev, { id: Date.now(), type: "text", pageIndex: textBox.pageIndex, x: textBox.x, y: textBox.y + fontSize, text: textBox.value, color, fontSize, opacity, strokeWidth: 1 }]); - setTextBox(null); + function commitTextBox(nextValue) { + setTextBox(current => { + if (!current) return null; + const value = typeof nextValue === "string" ? nextValue : current.value; + if (value.trim()) { + setAnnotations(prev => [...prev, { + id: Date.now(), + type: "text", + pageIndex: current.pageIndex, + x: current.x + TEXTBOX_PADDING_X, + y: current.y + TEXTBOX_PADDING_Y, + text: value, + color, + fontSize, + opacity, + strokeWidth: 1, + }]); + } + return null; + }); } // ── save PDF (applies both annotations and text edits) ────────────────────── @@ -241,7 +315,7 @@ export function EditPdf() {

Edit PDF

-

Draw, annotate, highlight — or click existing text to edit it directly in the browser.

+

Add text boxes, draw, and annotate your PDF pages — or switch modes to edit existing text already in the file.

{error &&
{error}
} { const f = fs[0]; if (f) { setFile(f); loadFile(f); } }} multiple={false} text="Drop a PDF to start editing" /> @@ -275,7 +349,7 @@ export function EditPdf() { Annotate @@ -283,10 +357,10 @@ export function EditPdf() { {mode === "annotate" && <>
- {ANN_TOOLS.map(({ id, label }) => ( - ))}
@@ -301,6 +375,12 @@ export function EditPdf() { onChange={e => tool==="text"?setFontSize(+e.target.value):setStroke(+e.target.value)} className="w-16 accent-white" /> {tool==="text"?fontSize:stroke}
+ {tool === "text" && ( +
+ + This adds a new text box. Click on the page to place it, then drag the placed text to adjust. +
+ )} } {/* Edit text mode info */} @@ -351,7 +431,15 @@ export function EditPdf() { {/* ── Page area ── */}
{pages.map((page, pi) => ( -
+
onPageClick(e, pi)} + onMouseMove={e => onMouseMove(e, pi)} + onMouseUp={e => onMouseUp(e, pi)} + onMouseLeave={e => onMouseUp(e, pi)} + >
Page {pi + 1} @@ -370,6 +458,35 @@ export function EditPdf() { onMouseLeave={e => onMouseUp(e, pi)} /> + {annotations.filter(a => a.pageIndex === pi && a.type === "text").map(ann => ( +
startTextDrag(e, ann)} + onClick={e => { + if (mode === "annotate" && tool === "eraser") { + e.stopPropagation(); + removeAnnotation(ann.id); + } + }} + style={{ + position:"absolute", + left:ann.x, + top:ann.y, + color:ann.color, + fontSize:ann.fontSize, + fontFamily:"sans-serif", + lineHeight:1.2, + whiteSpace:"pre-wrap", + cursor: mode === "annotate" && tool === "text" ? "move" : mode === "annotate" && tool === "eraser" ? "cell" : "default", + pointerEvents: mode === "annotate" && (tool === "text" || tool === "eraser") ? "auto" : "none", + zIndex:12, + userSelect:"none", + }} + > + {ann.text} +
+ ))} + {/* Text item overlays (Edit Text mode) */} {mode === "edittext" && textItems.filter(t => t.pageIndex === pi).map(item => { const isEditing = editingId === item.id; @@ -413,9 +530,10 @@ export function EditPdf() { {textBox?.pageIndex === pi && (