Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-04 11:56:27 -08:00
commit 598854e566
1 changed files with 118 additions and 30 deletions

View File

@ -1387,6 +1387,7 @@
} }
} }
folk-shape,
folk-markdown, folk-markdown,
folk-wrapper, folk-wrapper,
folk-arrow, folk-arrow,
@ -1428,6 +1429,15 @@
position: absolute; 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, .connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat,
folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map, folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map,
folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription, folk-image-gen, folk-video-gen, folk-prompt, folk-zine-gen, folk-transcription,
@ -2907,10 +2917,6 @@
if (document.getElementById(data.id)) { if (document.getElementById(data.id)) {
return; return;
} }
// Check if wb-svg already exists in the overlay
if (data.type === "wb-svg" && wbOverlay.querySelector(`[data-wb-id="${data.id}"]`)) {
return;
}
try { try {
isProcessingRemote = true; isProcessingRemote = true;
@ -3201,18 +3207,23 @@
if (data.maxItems) shape.maxItems = data.maxItems; if (data.maxItems) shape.maxItems = data.maxItems;
if (data.refreshInterval) shape.refreshInterval = data.refreshInterval; if (data.refreshInterval) shape.refreshInterval = data.refreshInterval;
break; break;
case "wb-svg": case "wb-svg": {
// Whiteboard SVG drawing — recreate in SVG overlay, not as a folk-shape // Whiteboard SVG drawing — render as a folk-shape with inline SVG
if (data.svgMarkup) { if (!data.svgMarkup) return null;
const temp = document.createElementNS("http://www.w3.org/2000/svg", "g"); let vb = data.svgViewBox;
temp.innerHTML = data.svgMarkup; // Old format: x/y/width/height are 0 — compute bounds from SVG
const svgEl = temp.firstElementChild; if (!data.width || !data.height || !vb) {
if (svgEl) { const bounds = computeWbBounds(data.svgMarkup);
svgEl.setAttribute("data-wb-id", data.id); if (!bounds) return null;
wbOverlay.appendChild(svgEl); 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": case "folk-markdown":
default: default:
shape = document.createElement("folk-markdown"); shape = document.createElement("folk-markdown");
@ -3917,12 +3928,57 @@
const wbColor = "#1e293b"; const wbColor = "#1e293b";
const wbStrokeWidth = 3; 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"); const wbOverlay = document.createElementNS("http://www.w3.org/2000/svg", "svg");
wbOverlay.id = "wb-overlay"; 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;"; wbOverlay.style.cssText = "position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:5;overflow:visible;";
canvasContent.appendChild(wbOverlay); 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) { function setWbTool(tool) {
wbTool = wbTool === tool ? null : tool; wbTool = wbTool === tool ? null : tool;
@ -3932,9 +3988,15 @@
canvas.style.cursor = ""; canvas.style.cursor = "";
} }
// Disable shape interaction when whiteboard tool is active // Disable shape interaction when drawing tools are active.
canvasContent.style.pointerEvents = wbTool ? "none" : ""; // Eraser keeps canvasContent interactive so it can delete wb-drawing shapes.
wbOverlay.style.pointerEvents = wbTool ? "all" : "none"; if (wbTool && wbTool !== "eraser") {
canvasContent.style.pointerEvents = "none";
wbOverlay.style.pointerEvents = "all";
} else {
canvasContent.style.pointerEvents = "";
wbOverlay.style.pointerEvents = wbTool === "eraser" ? "all" : "none";
}
syncBottomToolbar(); syncBottomToolbar();
} }
@ -4237,18 +4299,27 @@
if (!wbDrawing) return; if (!wbDrawing) return;
wbDrawing = false; wbDrawing = false;
// Persist the completed SVG element to Automerge // Convert the completed SVG drawing into a folk-shape
if (wbPreviewEl) { if (wbPreviewEl) {
const wbId = `wb-${Date.now()}-${++shapeCounter}`;
wbPreviewEl.setAttribute("data-wb-id", wbId);
const svgMarkup = wbPreviewEl.outerHTML; const svgMarkup = wbPreviewEl.outerHTML;
const bounds = computeWbBounds(svgMarkup);
sync.addShapeData({ // Remove the temporary preview from the overlay
type: "wb-svg", wbPreviewEl.remove();
id: wbId,
svgMarkup, if (bounds) {
x: 0, y: 0, width: 0, height: 0, rotation: 0, 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; wbPreviewEl = null;
@ -4260,8 +4331,7 @@
} }
}); });
// Eraser click fallback — deletion is handled in pointerdown above, // Eraser click fallback for old SVG overlay elements
// but catch any clicks that slip through (e.g. keyboard-triggered)
wbOverlay.addEventListener("click", (e) => { wbOverlay.addEventListener("click", (e) => {
if (wbTool !== "eraser") return; if (wbTool !== "eraser") return;
const hit = e.target; 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 ── // ── Helper: get local user DID ──
function getLocalDID() { function getLocalDID() {
try { try {