+
+ {/* ── Toolbar ── */}
+
+
+ {/* Pen / Eraser */}
+
+
setIsEraser(false)}
+ className={cn(
+ "p-1.5 rounded-md transition-colors",
+ !isEraser ? "bg-zinc-700 text-white shadow" : "text-zinc-500 hover:text-zinc-300"
+ )}
+ title="Pen (P)"
+ >
+
+
+
setIsEraser(true)}
+ className={cn(
+ "p-1.5 rounded-md transition-colors",
+ isEraser ? "bg-zinc-700 text-red-400 shadow" : "text-zinc-500 hover:text-zinc-300"
+ )}
+ title="Eraser (E)"
+ >
+
+
+
+
+
+
+ {/* Colours */}
+
+ {COLORS.map((c) => (
+ { setColor(c); setIsEraser(false); }}
+ className={cn(
+ "rounded-full border-2 transition-all",
+ color === c && !isEraser ? "border-amber-400 scale-125" : "border-zinc-700 hover:border-zinc-400"
+ )}
+ style={{ width: 20, height: 20, backgroundColor: c }}
+ />
+ ))}
+
+
+
+
+ {/* Brush sizes */}
+
+ {BRUSH_SIZES.map((s) => (
+
setBrushSize(s)}
+ className={cn(
+ "flex items-center justify-center rounded-full bg-zinc-800 transition-all",
+ brushSize === s ? "ring-2 ring-amber-400" : "opacity-50 hover:opacity-100"
+ )}
+ style={{ width: s + 10, height: s + 10 }}
+ title={`${s}px`}
+ >
+
+
+ ))}
+
+
+
+
+ {/* Template selector */}
+
setShowTemplatePicker(true)}
+ className={cn(
+ "flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors",
+ activeTemplate
+ ? "bg-amber-500/15 border border-amber-600/40 text-amber-400"
+ : "text-zinc-500 hover:text-zinc-200 hover:bg-zinc-800"
+ )}
+ title="Background template"
+ >
+
+ {activeTemplate ? activeTemplate.name : "Template"}
+
+ {activeTemplate && (
+
setActiveTemplate(null)}
+ className="p-1 -ml-2 text-zinc-600 hover:text-red-400 transition-colors"
+ title="Remove template"
+ >
+
+
+ )}
+
+
+
+ {error &&
{error} }
+
+ {/* Actions */}
+
+
+ Undo
+
+
+ Clear
+
+
+ {saving ? : }
+ {saving ? "Saving…" : "Save Sketch"}
+
+
+
+
+
+
+
+ {/* ── Canvas area ── */}
+
+ {/* Background layer: BG_COLOR + optional template */}
+
+
+ {/* Drawing layer: transparent, receives pointer events */}
+
+
+ {strokeCount === 0 && !activeTemplate && (
+
+ )}
+
+ {/* Template picker modal */}
+ {showTemplatePicker && (
+
{ setActiveTemplate(t); setShowTemplatePicker(false); }}
+ onClear={() => { setActiveTemplate(null); setShowTemplatePicker(false); }}
+ onClose={() => setShowTemplatePicker(false)}
+ />
+ )}
+
+
+ );
+}
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
diff --git a/prisma/migrations/20260712000001_add_sketch_templates/migration.sql b/prisma/migrations/20260712000001_add_sketch_templates/migration.sql
new file mode 100644
index 0000000..dedc9fa
--- /dev/null
+++ b/prisma/migrations/20260712000001_add_sketch_templates/migration.sql
@@ -0,0 +1,13 @@
+-- CreateTable
+CREATE TABLE "sketch_templates" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "fileUrl" TEXT NOT NULL,
+ "fileKey" TEXT NOT NULL DEFAULT '',
+ "fileName" TEXT NOT NULL DEFAULT '',
+ "fileSize" BIGINT,
+ "sortOrder" INTEGER NOT NULL DEFAULT 0,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "sketch_templates_pkey" PRIMARY KEY ("id")
+);
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 838dec8..9bcbf7d 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -751,3 +751,16 @@ model TakeAttachment {
@@map("take_attachments")
}
+
+model SketchTemplate {
+ id String @id @default(cuid())
+ name String
+ fileUrl String
+ fileKey String @default("")
+ fileName String @default("")
+ fileSize BigInt?
+ sortOrder Int @default(0)
+ createdAt DateTime @default(now())
+
+ @@map("sketch_templates")
+}