mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 16:03:10 +07:00
bổ sung chức năng eraser
This commit is contained in:
@@ -8,7 +8,7 @@ import { api } from "@/lib/api";
|
||||
// Types
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type Tool = "select" | "pen" | "highlighter" | "text";
|
||||
type Tool = "select" | "pen" | "highlighter" | "text" | "eraser";
|
||||
type ViewMode = "pan" | "draw";
|
||||
type FitMode = "width" | "height";
|
||||
type ScrollMode = "single" | "continuous";
|
||||
@@ -565,6 +565,18 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
fc.renderAll();
|
||||
});
|
||||
break;
|
||||
|
||||
case "eraser":
|
||||
fc.isDrawingMode = false;
|
||||
fc.selection = false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fc.on("mouse:down", (options: any) => {
|
||||
if (!options.target) return;
|
||||
fc.remove(options.target);
|
||||
addedObjects.current = addedObjects.current.filter(o => o !== options.target);
|
||||
fc.renderAll();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [isReady, mode, tool, penColor, strokeWidth]);
|
||||
|
||||
@@ -602,6 +614,12 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
const t = new fabric.IText("Text", { left: pointer.x, top: pointer.y, fontSize: 20, fill: penColorRef.current, fontFamily: "Arial, sans-serif", padding: 4 });
|
||||
fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll();
|
||||
}); break;
|
||||
case "eraser":
|
||||
fc.isDrawingMode = false; fc.selection = false;
|
||||
fc.on("mouse:down", (options: any) => {
|
||||
if (!options.target) return;
|
||||
fc.remove(options.target); fc.renderAll();
|
||||
}); break;
|
||||
}
|
||||
});
|
||||
}, [isReady, scrollMode, mode, tool, penColor, strokeWidth]);
|
||||
@@ -700,254 +718,244 @@ export default function WorkbookViewer({ pdfId }: Props) {
|
||||
|
||||
{/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
|
||||
<header className="bg-white border-b shadow-sm sticky top-0 z-20">
|
||||
<div className="max-w-5xl mx-auto px-3 py-2 flex flex-wrap items-center gap-2">
|
||||
<div className="px-3 py-2 flex items-center gap-0">
|
||||
|
||||
{/* Back button + title */}
|
||||
<button
|
||||
onClick={() => router.push("/dashboard")}
|
||||
title="Back"
|
||||
className="p-1 text-gray-500 hover:text-blue-600 transition rounded"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<span className="text-sm font-semibold text-gray-800 truncate max-w-[130px] sm:max-w-xs">
|
||||
{pdfTitle}
|
||||
</span>
|
||||
{/* Scrollable tools strip */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto flex-1 min-w-0">
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
|
||||
|
||||
{/* Thumbnail panel toggle */}
|
||||
<button
|
||||
onClick={() => setShowThumbnails(v => !v)}
|
||||
title="Thumbnails"
|
||||
className={`p-1.5 rounded-lg transition flex items-center gap-1 ${
|
||||
showThumbnails
|
||||
? "bg-blue-50 text-blue-600"
|
||||
: "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium hidden sm:inline">Thumb</span>
|
||||
</button>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
|
||||
|
||||
{/* Fit mode buttons */}
|
||||
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5">
|
||||
{/* Back button + title */}
|
||||
<button
|
||||
onClick={() => setFitMode("width")}
|
||||
title="Fit Width"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
fitMode === "width" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
onClick={() => router.push("/dashboard")}
|
||||
title="Back"
|
||||
className="p-1 text-gray-500 hover:text-blue-600 transition rounded flex-shrink-0"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Width</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFitMode("height")}
|
||||
title="Fit Height"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
fitMode === "height" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8V4m0 0H3m4 0l-4 4M17 8V4m0 0h4m-4 0l4 4M7 16v4m0 0H3m4 0l-4-4M17 16v4m0 0h4m-4 0l4-4" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Height</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
|
||||
|
||||
{/* Scroll mode toggle */}
|
||||
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => setScrollMode("single")}
|
||||
title="Single page"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
scrollMode === "single" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6M9 8h6M9 16h6M5 4h14a1 1 0 011 1v14a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1z" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Single</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setScrollMode("continuous")}
|
||||
title="Continuous scroll"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
scrollMode === "continuous" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
<span className="hidden sm:inline">Continuous</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
|
||||
|
||||
{/* Page navigation */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage <= 1 || rendering || !isReady}
|
||||
className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition"
|
||||
title="Previous page"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{isReady ? (
|
||||
<span className="flex items-center gap-1 text-xs text-gray-600 tabular-nums">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
value={currentPage}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
if (v >= 1 && v <= totalPages) goToPage(v);
|
||||
}}
|
||||
onFocus={(e) => e.target.select()}
|
||||
className="w-10 text-center text-xs border border-gray-300 rounded px-1 py-0.5 focus:outline-none focus:border-blue-500 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<span>/ {totalPages}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-600 px-1">…</span>
|
||||
)}
|
||||
<span className="text-sm font-semibold text-gray-800 truncate max-w-[100px] flex-shrink-0">
|
||||
{pdfTitle}
|
||||
</span>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
{/* Thumbnail toggle */}
|
||||
<button
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages || rendering || !isReady}
|
||||
className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition"
|
||||
title="Next page"
|
||||
onClick={() => setShowThumbnails(v => !v)}
|
||||
title="Thumbnails"
|
||||
className={`p-1.5 rounded-lg transition flex items-center gap-1 flex-shrink-0 ${
|
||||
showThumbnails ? "bg-blue-50 text-blue-600" : "text-gray-500 hover:bg-gray-100 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 hidden sm:block" />
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
{/* Pan / Draw mode toggle */}
|
||||
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5">
|
||||
<button
|
||||
onClick={() => setViewMode("pan")}
|
||||
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
|
||||
mode === "pan" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
Pan
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("draw")}
|
||||
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
|
||||
mode === "draw" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
Draw
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Drawing tools — only shown in Draw mode */}
|
||||
{mode === "draw" && (
|
||||
<>
|
||||
<div className="h-5 w-px bg-gray-200" />
|
||||
|
||||
{/* Tool buttons */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{/* Select */}
|
||||
<ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
||||
</ToolBtn>
|
||||
|
||||
{/* Pen */}
|
||||
<ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</ToolBtn>
|
||||
|
||||
{/* Highlighter */}
|
||||
<ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</ToolBtn>
|
||||
|
||||
{/* Text */}
|
||||
<ToolBtn id="text" current={tool} onClick={setTool} title="Text">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 12h6m-3-3v6M7 20h10a2 2 0 002-2V6a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</ToolBtn>
|
||||
</div>
|
||||
|
||||
{/* Color picker */}
|
||||
<input
|
||||
type="color"
|
||||
value={penColor}
|
||||
onChange={(e) => setPenColor(e.target.value)}
|
||||
title="Color"
|
||||
className="w-7 h-7 rounded cursor-pointer border border-gray-300 p-0.5 bg-white"
|
||||
/>
|
||||
|
||||
{/* Stroke width slider */}
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={12}
|
||||
value={strokeWidth}
|
||||
onChange={(e) => setStrokeWidth(Number(e.target.value))}
|
||||
className="w-16 accent-blue-600"
|
||||
title={`Stroke width: ${strokeWidth}`}
|
||||
/>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200" />
|
||||
|
||||
{/* Undo */}
|
||||
{/* Fit mode */}
|
||||
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
title="Undo last stroke"
|
||||
className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition"
|
||||
onClick={() => setFitMode("width")}
|
||||
title="Fit Width"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
fitMode === "width" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h8M8 12h8M4 17h16" />
|
||||
</svg>
|
||||
<span className="hidden lg:inline">Width</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFitMode("height")}
|
||||
title="Fit Height"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
fitMode === "height" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8V4m0 0H3m4 0l-4 4M17 8V4m0 0h4m-4 0l4 4M7 16v4m0 0H3m4 0l-4-4M17 16v4m0 0h4m-4 0l4-4" />
|
||||
</svg>
|
||||
<span className="hidden lg:inline">Height</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
{/* Scroll mode */}
|
||||
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setScrollMode("single")}
|
||||
title="Single page"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
scrollMode === "single" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6M9 8h6M9 16h6M5 4h14a1 1 0 011 1v14a1 1 0 01-1 1H5a1 1 0 01-1-1V5a1 1 0 011-1z" />
|
||||
</svg>
|
||||
<span className="hidden lg:inline">Single</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setScrollMode("continuous")}
|
||||
title="Continuous scroll"
|
||||
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${
|
||||
scrollMode === "continuous" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
<span className="hidden lg:inline">Continuous</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
{/* Page navigation */}
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage <= 1 || rendering || !isReady}
|
||||
className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition"
|
||||
title="Previous page"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Clear page */}
|
||||
{isReady ? (
|
||||
<span className="flex items-center gap-1 text-xs text-gray-600 tabular-nums">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
value={currentPage}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value);
|
||||
if (v >= 1 && v <= totalPages) goToPage(v);
|
||||
}}
|
||||
onFocus={(e) => e.target.select()}
|
||||
className="w-10 text-center text-xs border border-gray-300 rounded px-1 py-0.5 focus:outline-none focus:border-blue-500 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<span>/ {totalPages}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-600 px-1">…</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleClear}
|
||||
title="Clear page annotations"
|
||||
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition"
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages || rendering || !isReady}
|
||||
className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition"
|
||||
title="Next page"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M4 7h16M10 3h4a1 1 0 011 1v1H9V4a1 1 0 011-1z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Save button — far right */}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
{/* Pan / Draw mode toggle */}
|
||||
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => setViewMode("pan")}
|
||||
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
|
||||
mode === "pan" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
Pan
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("draw")}
|
||||
className={`text-xs px-2.5 py-1 rounded-md font-medium transition ${
|
||||
mode === "draw" ? "bg-white shadow text-blue-600" : "text-gray-500 hover:text-gray-800"
|
||||
}`}
|
||||
>
|
||||
Draw
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Drawing tools — inline, visible only in draw mode */}
|
||||
{mode === "draw" && (
|
||||
<>
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
<ToolBtn id="select" current={tool} onClick={setTool} title="Select / Move">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
||||
</ToolBtn>
|
||||
<ToolBtn id="pen" current={tool} onClick={setTool} title="Pen">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</ToolBtn>
|
||||
<ToolBtn id="highlighter" current={tool} onClick={setTool} title="Highlighter">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
||||
</ToolBtn>
|
||||
<ToolBtn id="text" current={tool} onClick={setTool} title="Text">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 12h6m-3-3v6M7 20h10a2 2 0 002-2V6a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</ToolBtn>
|
||||
<ToolBtn id="eraser" current={tool} onClick={setTool} title="Eraser">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M19 7l-1.5 1.5M4.5 19.5l9-9m0 0L9 6l-4.5 4.5 4.5 4.5m4.5-4.5L18 6" />
|
||||
</ToolBtn>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="color"
|
||||
value={penColor}
|
||||
onChange={(e) => setPenColor(e.target.value)}
|
||||
title="Color"
|
||||
className="w-7 h-7 rounded cursor-pointer border border-gray-300 p-0.5 bg-white flex-shrink-0"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={12}
|
||||
value={strokeWidth}
|
||||
onChange={(e) => setStrokeWidth(Number(e.target.value))}
|
||||
className="w-20 accent-blue-600 flex-shrink-0"
|
||||
title={`Stroke width: ${strokeWidth}`}
|
||||
/>
|
||||
|
||||
<div className="h-5 w-px bg-gray-200 flex-shrink-0" />
|
||||
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
title="Undo last stroke"
|
||||
className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition flex-shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleClear}
|
||||
title="Clear page annotations"
|
||||
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition flex-shrink-0"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M4 7h16M10 3h4a1 1 0 011 1v1H9V4a1 1 0 011-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Save — fixed right, never scrolls away */}
|
||||
<div className="flex items-center gap-2 pl-3 flex-shrink-0">
|
||||
{saveNotice && (
|
||||
<span className="text-xs text-green-600 font-medium animate-pulse">
|
||||
✓ Saved
|
||||
</span>
|
||||
<span className="text-xs text-green-600 font-medium animate-pulse">✓ Saved</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
|
||||
Reference in New Issue
Block a user