diff --git a/lib/community-sync.ts b/lib/community-sync.ts index aaad860..3429c56 100644 --- a/lib/community-sync.ts +++ b/lib/community-sync.ts @@ -25,6 +25,8 @@ export interface ShapeData { isMinimized?: boolean; isPinned?: boolean; tags?: string[]; + // Whiteboard SVG drawing + svgMarkup?: string; // Allow arbitrary shape-specific properties from toJSON() [key: string]: unknown; } @@ -495,6 +497,18 @@ export class CommunitySync extends EventTarget { this.forgetShape(shapeId); } + /** + * Add raw shape data directly (for shapes without DOM elements, like wb-svg drawings). + */ + addShapeData(shapeData: ShapeData): void { + this.#doc = Automerge.change(this.#doc, `Add shape ${shapeData.id}`, (doc) => { + if (!doc.shapes) doc.shapes = {}; + doc.shapes[shapeData.id] = shapeData; + }); + this.#scheduleSave(); + this.#syncToServer(); + } + /** * FUN: Update — explicitly update specific fields of a shape. * Use this for programmatic updates (API calls, module callbacks). diff --git a/lib/folk-markdown.ts b/lib/folk-markdown.ts index 9bdec1c..8636d57 100644 --- a/lib/folk-markdown.ts +++ b/lib/folk-markdown.ts @@ -186,23 +186,51 @@ export class FolkMarkdown extends FolkShape { const editBtn = wrapper.querySelector(".edit-btn") as HTMLButtonElement; const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; - // Edit toggle + // Helper to enter/exit markdown edit mode + const enterMarkdownEdit = () => { + if (this.#isEditing) return; + this.#isEditing = true; + editor.style.display = "block"; + preview.style.display = "none"; + editor.value = this.#content; + editor.focus(); + }; + + const exitMarkdownEdit = () => { + if (!this.#isEditing) return; + this.#isEditing = false; + editor.style.display = "none"; + preview.style.display = "block"; + this.content = editor.value; + preview.innerHTML = this.#renderMarkdown(this.#content); + }; + + // Edit toggle button editBtn.addEventListener("click", (e) => { e.stopPropagation(); - this.#isEditing = !this.#isEditing; if (this.#isEditing) { - editor.style.display = "block"; - preview.style.display = "none"; - editor.value = this.#content; - editor.focus(); + exitMarkdownEdit(); } else { - editor.style.display = "none"; - preview.style.display = "block"; - this.content = editor.value; - preview.innerHTML = this.#renderMarkdown(this.#content); + enterMarkdownEdit(); } }); + // Click on preview enters edit mode (when shape is focused/editing) + preview.addEventListener("click", (e) => { + e.stopPropagation(); + enterMarkdownEdit(); + }); + + // When parent shape enters edit mode, also enter markdown edit + this.addEventListener("edit-enter", () => { + enterMarkdownEdit(); + }); + + // When parent shape exits edit mode, also exit markdown edit + this.addEventListener("edit-exit", () => { + exitMarkdownEdit(); + }); + // Close button closeBtn.addEventListener("click", (e) => { e.stopPropagation(); @@ -215,11 +243,7 @@ export class FolkMarkdown extends FolkShape { }); editor.addEventListener("blur", () => { - this.#isEditing = false; - editor.style.display = "none"; - preview.style.display = "block"; - this.content = editor.value; - preview.innerHTML = this.#renderMarkdown(this.#content); + exitMarkdownEdit(); }); // Initial render diff --git a/website/canvas.html b/website/canvas.html index f0f9fed..016c3e6 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1240,6 +1240,10 @@ 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; @@ -1262,6 +1266,9 @@ if (shape && shape.parentNode) { shape.remove(); } + // Also remove wb-svg elements from the overlay + const wbEl = wbOverlay?.querySelector(`[data-wb-id="${shapeId}"]`); + if (wbEl) wbEl.remove(); }); // Create a shape element from data @@ -1487,6 +1494,18 @@ 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); + } + } + return null; // Not a folk-shape element case "folk-markdown": default: shape = document.createElement("folk-markdown"); @@ -2170,15 +2189,34 @@ wbOverlay.addEventListener("pointerup", (e) => { if (!wbDrawing) return; wbDrawing = false; + + // Persist the completed SVG element to Automerge + if (wbPreviewEl) { + const wbId = `wb-${Date.now()}-${++shapeCounter}`; + wbPreviewEl.setAttribute("data-wb-id", wbId); + const svgMarkup = wbPreviewEl.outerHTML; + + sync.addShapeData({ + type: "wb-svg", + id: wbId, + svgMarkup, + x: 0, y: 0, width: 0, height: 0, rotation: 0, + }); + } + wbPreviewEl = null; wbCurrentPath = []; }); - // Eraser: click on existing SVG strokes to delete them + // Eraser: click on existing SVG strokes to delete them + remove from Automerge wbOverlay.addEventListener("click", (e) => { if (wbTool !== "eraser") return; const hit = e.target; if (hit && hit !== wbOverlay && wbOverlay.contains(hit)) { + const wbId = hit.getAttribute("data-wb-id"); + if (wbId) { + sync.deleteShape(wbId); + } hit.remove(); } }); @@ -2407,6 +2445,21 @@ } }); + // Desktop quick-add button → opens the rApps popout panel + document.getElementById("quick-add")?.addEventListener("click", (e) => { + e.stopPropagation(); + const rAppsGroup = toolbarEl.querySelector(".toolbar-group:has(#embed-notes)") + || [...toolbarEl.querySelectorAll(".toolbar-group")].find(g => + g.querySelector(".toolbar-group-toggle")?.textContent.includes("rApps")); + if (rAppsGroup) { + if (activeToolbarGroup === rAppsGroup) { + closeToolbarPanel(); + } else { + openToolbarPanel(rAppsGroup); + } + } + }); + // Collapse/expand toolbar const collapseBtn = document.getElementById("toolbar-collapse"); collapseBtn.addEventListener("click", () => { @@ -2556,6 +2609,13 @@ canvas.style.cursor = ""; }); + // Double-click on empty canvas background → quick-draw (pencil) mode + canvas.addEventListener("dblclick", (e) => { + if (e.target === canvas || e.target === canvasContent) { + setWbTool("pencil"); + } + }); + // Keep-alive ping to prevent WebSocket idle timeout setInterval(() => { try {