bổ sung chức năng eraser

This commit is contained in:
2026-04-01 09:25:31 +07:00
parent 79eea344e9
commit 89db1c7b5c
+231 -223
View File
@@ -8,7 +8,7 @@ import { api } from "@/lib/api";
// Types // Types
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
type Tool = "select" | "pen" | "highlighter" | "text"; type Tool = "select" | "pen" | "highlighter" | "text" | "eraser";
type ViewMode = "pan" | "draw"; type ViewMode = "pan" | "draw";
type FitMode = "width" | "height"; type FitMode = "width" | "height";
type ScrollMode = "single" | "continuous"; type ScrollMode = "single" | "continuous";
@@ -565,6 +565,18 @@ export default function WorkbookViewer({ pdfId }: Props) {
fc.renderAll(); fc.renderAll();
}); });
break; 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]); }, [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 }); 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(); fc.add(t); fc.setActiveObject(t); t.enterEditing(); t.selectAll(); fc.renderAll();
}); break; }); 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]); }, [isReady, scrollMode, mode, tool, penColor, strokeWidth]);
@@ -700,254 +718,244 @@ export default function WorkbookViewer({ pdfId }: Props) {
{/* ── Sticky Toolbar ──────────────────────────────────────────────────── */} {/* ── Sticky Toolbar ──────────────────────────────────────────────────── */}
<header className="bg-white border-b shadow-sm sticky top-0 z-20"> <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 */} {/* Scrollable tools strip */}
<button <div className="flex items-center gap-2 overflow-x-auto flex-1 min-w-0">
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>
<div className="h-5 w-px bg-gray-200 hidden sm:block" /> {/* Back button + title */}
{/* 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">
<button <button
onClick={() => setFitMode("width")} onClick={() => router.push("/dashboard")}
title="Fit Width" title="Back"
className={`text-xs px-2 py-1 rounded-md font-medium transition flex items-center gap-1 ${ className="p-1 text-gray-500 hover:text-blue-600 transition rounded flex-shrink-0"
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"> <svg className="w-5 h-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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg> </svg>
</button> </button>
{isReady ? ( <span className="text-sm font-semibold text-gray-800 truncate max-w-[100px] flex-shrink-0">
<span className="flex items-center gap-1 text-xs text-gray-600 tabular-nums"> {pdfTitle}
<input </span>
type="number"
min={1} <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
max={totalPages}
value={currentPage} {/* Thumbnail toggle */}
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 <button
onClick={() => goToPage(currentPage + 1)} onClick={() => setShowThumbnails(v => !v)}
disabled={currentPage >= totalPages || rendering || !isReady} title="Thumbnails"
className="p-1 rounded hover:bg-gray-100 disabled:opacity-40 transition" className={`p-1.5 rounded-lg transition flex items-center gap-1 flex-shrink-0 ${
title="Next page" 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"> <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> </svg>
</button> </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 */} {/* Fit mode */}
<div className="flex items-center gap-0.5 bg-gray-100 rounded-lg p-0.5"> <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 — 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 */}
<button <button
onClick={handleUndo} onClick={() => setFitMode("width")}
title="Undo last stroke" title="Fit Width"
className="p-1.5 text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded-lg transition" 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"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg> </svg>
</button> </button>
{isReady ? (
{/* Clear page */} <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 <button
onClick={handleClear} onClick={() => goToPage(currentPage + 1)}
title="Clear page annotations" disabled={currentPage >= totalPages || rendering || !isReady}
className="p-1.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition" 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"> <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
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> </svg>
</button> </button>
</> </div>
)}
{/* Save button — far right */} <div className="h-5 w-px bg-gray-200 flex-shrink-0" />
<div className="ml-auto flex items-center gap-2">
{/* 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 && ( {saveNotice && (
<span className="text-xs text-green-600 font-medium animate-pulse"> <span className="text-xs text-green-600 font-medium animate-pulse"> Saved</span>
Saved
</span>
)} )}
<button <button
onClick={handleSave} onClick={handleSave}