diff --git a/website/canvas.html b/website/canvas.html
index d831bc9..fe67b22 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -1387,6 +1387,7 @@
}
}
+ folk-shape,
folk-markdown,
folk-wrapper,
folk-arrow,
@@ -1428,6 +1429,15 @@
position: absolute;
}
+ /* Whiteboard drawings rendered as folk-shapes with inline SVG */
+ folk-shape[data-wb-drawing] > svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+ overflow: visible;
+ pointer-events: none;
+ }
+
.connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat,
folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map,
folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription,
@@ -2907,10 +2917,6 @@
if (document.getElementById(data.id)) {
return;
}
- // Check if wb-svg already exists in the overlay
- if (data.type === "wb-svg" && wbOverlay.querySelector(`[data-wb-id="${data.id}"]`)) {
- return;
- }
try {
isProcessingRemote = true;
@@ -3201,18 +3207,23 @@
if (data.maxItems) shape.maxItems = data.maxItems;
if (data.refreshInterval) shape.refreshInterval = data.refreshInterval;
break;
- case "wb-svg":
- // Whiteboard SVG drawing — recreate in SVG overlay, not as a folk-shape
- if (data.svgMarkup) {
- const temp = document.createElementNS("http://www.w3.org/2000/svg", "g");
- temp.innerHTML = data.svgMarkup;
- const svgEl = temp.firstElementChild;
- if (svgEl) {
- svgEl.setAttribute("data-wb-id", data.id);
- wbOverlay.appendChild(svgEl);
- }
+ case "wb-svg": {
+ // Whiteboard SVG drawing — render as a folk-shape with inline SVG
+ if (!data.svgMarkup) return null;
+ let vb = data.svgViewBox;
+ // Old format: x/y/width/height are 0 — compute bounds from SVG
+ if (!data.width || !data.height || !vb) {
+ const bounds = computeWbBounds(data.svgMarkup);
+ if (!bounds) return null;
+ data.x = bounds.x;
+ data.y = bounds.y;
+ data.width = bounds.width;
+ data.height = bounds.height;
+ vb = bounds.viewBox;
}
- return null; // Not a folk-shape element
+ shape = createWbShapeElement(data.svgMarkup, vb);
+ break;
+ }
case "folk-markdown":
default:
shape = document.createElement("folk-markdown");
@@ -3917,12 +3928,57 @@
const wbColor = "#1e293b";
const wbStrokeWidth = 3;
- // SVG overlay for whiteboard drawing
+ // SVG overlay for whiteboard drawing (used during active drawing only)
const wbOverlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
wbOverlay.id = "wb-overlay";
wbOverlay.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible;";
canvasContent.appendChild(wbOverlay);
+ // ── Helpers for converting SVG drawings into folk-shape elements ──
+
+ // Compute bounding box of SVG markup by temporarily rendering it
+ function computeWbBounds(svgMarkup, padding) {
+ if (padding === undefined) padding = wbStrokeWidth + 2;
+ const temp = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ temp.innerHTML = svgMarkup;
+ const el = temp.firstElementChild;
+ if (!el) return null;
+ wbOverlay.appendChild(el);
+ const bbox = el.getBBox();
+ el.remove();
+ if (bbox.width < 1 && bbox.height < 1) return null;
+ return {
+ x: bbox.x - padding,
+ y: bbox.y - padding,
+ width: bbox.width + padding * 2,
+ height: bbox.height + padding * 2,
+ viewBox: `${bbox.x - padding} ${bbox.y - padding} ${bbox.width + padding * 2} ${bbox.height + padding * 2}`,
+ };
+ }
+
+ // Create a folk-shape element wrapping SVG drawing content
+ function createWbShapeElement(svgMarkup, viewBox) {
+ const shape = document.createElement("folk-shape");
+ shape.dataset.wbDrawing = "true";
+
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ svg.setAttribute("viewBox", viewBox);
+ svg.setAttribute("preserveAspectRatio", "none");
+ svg.innerHTML = svgMarkup;
+ shape.appendChild(svg);
+
+ // Monkey-patch toJSON so CommunitySync persists svgMarkup
+ shape.toJSON = function() {
+ return {
+ type: "wb-svg",
+ svgMarkup: svgMarkup,
+ svgViewBox: viewBox,
+ };
+ };
+
+ return shape;
+ }
+
function setWbTool(tool) {
wbTool = wbTool === tool ? null : tool;
@@ -3932,9 +3988,15 @@
canvas.style.cursor = "";
}
- // Disable shape interaction when whiteboard tool is active
- canvasContent.style.pointerEvents = wbTool ? "none" : "";
- wbOverlay.style.pointerEvents = wbTool ? "all" : "none";
+ // Disable shape interaction when drawing tools are active.
+ // Eraser keeps canvasContent interactive so it can delete wb-drawing shapes.
+ if (wbTool && wbTool !== "eraser") {
+ canvasContent.style.pointerEvents = "none";
+ wbOverlay.style.pointerEvents = "all";
+ } else {
+ canvasContent.style.pointerEvents = "";
+ wbOverlay.style.pointerEvents = wbTool === "eraser" ? "all" : "none";
+ }
syncBottomToolbar();
}
@@ -4237,18 +4299,27 @@
if (!wbDrawing) return;
wbDrawing = false;
- // Persist the completed SVG element to Automerge
+ // Convert the completed SVG drawing into a folk-shape
if (wbPreviewEl) {
- const wbId = `wb-${Date.now()}-${++shapeCounter}`;
- wbPreviewEl.setAttribute("data-wb-id", wbId);
const svgMarkup = wbPreviewEl.outerHTML;
+ const bounds = computeWbBounds(svgMarkup);
- sync.addShapeData({
- type: "wb-svg",
- id: wbId,
- svgMarkup,
- x: 0, y: 0, width: 0, height: 0, rotation: 0,
- });
+ // Remove the temporary preview from the overlay
+ wbPreviewEl.remove();
+
+ if (bounds) {
+ const wbId = `wb-${Date.now()}-${++shapeCounter}`;
+ const shape = createWbShapeElement(svgMarkup, bounds.viewBox);
+ shape.id = wbId;
+ shape.x = bounds.x;
+ shape.y = bounds.y;
+ shape.width = bounds.width;
+ shape.height = bounds.height;
+
+ setupShapeEventListeners(shape);
+ canvasContent.appendChild(shape);
+ sync.registerShape(shape);
+ }
}
wbPreviewEl = null;
@@ -4260,8 +4331,7 @@
}
});
- // Eraser click fallback — deletion is handled in pointerdown above,
- // but catch any clicks that slip through (e.g. keyboard-triggered)
+ // Eraser click fallback for old SVG overlay elements
wbOverlay.addEventListener("click", (e) => {
if (wbTool !== "eraser") return;
const hit = e.target;
@@ -4272,6 +4342,24 @@
}
});
+ // Eraser for wb-drawing folk-shapes — capturing phase intercepts
+ // before normal shape interaction (drag/select) kicks in
+ canvasContent.addEventListener("pointerdown", (e) => {
+ if (wbTool !== "eraser") return;
+ const target = e.target.closest?.("[data-wb-drawing]");
+ if (!target) return;
+ e.stopPropagation();
+ e.preventDefault();
+ const state = sync.getShapeVisualState(target.id);
+ if (state === "forgotten") {
+ sync.hardDeleteShape(target.id);
+ target.remove();
+ } else {
+ sync.forgetShape(target.id, getLocalDID());
+ target.forgotten = true;
+ }
+ }, true);
+
// ── Helper: get local user DID ──
function getLocalDID() {
try {