fix: canvas tools place shapes exactly where the ghost placeholder shows

- newShape() with explicit position now places directly at click point
  instead of routing through findFreePosition spiral which could nudge
  the shape away from the cursor
- Sticky note converted to setPendingTool so it shows dotted placeholder
  before placement instead of instantly spawning at viewport center
- Feed tool converted to setPendingTool with __postCreate for the same
  click-to-place UX
- Removed wb-sticky from mobile keepOpen list since it's now a
  placement tool that should close the menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-28 22:42:33 -08:00
parent 47b585665d
commit 8d77c6eee8
1 changed files with 57 additions and 51 deletions

View File

@ -2053,8 +2053,9 @@
}
});
// Create a shape, position it without overlapping others, add to canvas, and register for sync
// atPosition: optional { x, y } in canvas coordinates to place near
// Create a shape, add to canvas, and register for sync.
// atPosition: optional { x, y } in canvas coordinates — places shape centered there exactly.
// Without atPosition, uses findFreePosition to auto-place without overlapping.
function newShape(tagName, props = {}, atPosition) {
const id = `shape-${Date.now()}-${++shapeCounter}`;
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
@ -2062,9 +2063,14 @@
const shape = document.createElement(tagName);
shape.id = id;
const pos = atPosition
? findFreePosition(defaults.width, defaults.height, atPosition.x, atPosition.y)
: findFreePosition(defaults.width, defaults.height);
let pos;
if (atPosition) {
// Explicit click-to-place: honor exact position (centered on click)
pos = { x: atPosition.x - defaults.width / 2, y: atPosition.y - defaults.height / 2 };
} else {
// Auto-place: find free spot near viewport center
pos = findFreePosition(defaults.width, defaults.height);
}
shape.x = pos.x;
shape.y = pos.y;
shape.width = defaults.width;
@ -2280,48 +2286,49 @@
};
const flowKind = moduleFlowKinds[sourceModule] || "data";
const shape = newShape("folk-feed", {
setPendingTool("folk-feed", {
sourceModule,
sourceLayer: "layer-" + sourceModule,
feedId: "",
flowKind,
maxItems: 10,
refreshInterval: 30000,
__postCreate: (shape) => {
// Auto-register a LayerFlow in Automerge if layers exist
if (sync.getLayers) {
const layers = sync.getLayers();
const currentLayer = layers.find(l => l.moduleId === "rspace") || layers[0];
const sourceLayer = layers.find(l => l.moduleId === sourceModule);
if (currentLayer && sourceLayer) {
const flowId = `flow-auto-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
sync.addFlow({
id: flowId,
kind: flowKind,
sourceLayerId: sourceLayer.id,
targetLayerId: currentLayer.id,
targetShapeId: shape.id,
label: sourceModule + " feed",
strength: 0.5,
active: true,
});
}
// Also ensure source module has a layer (add if missing)
if (!sourceLayer) {
sync.addLayer({
id: "layer-" + sourceModule,
moduleId: sourceModule,
label: sourceModule,
order: layers.length,
color: "",
visible: true,
createdAt: Date.now(),
});
}
}
},
});
// Auto-register a LayerFlow in Automerge if layers exist
if (shape && sync.getLayers) {
const layers = sync.getLayers();
const currentLayer = layers.find(l => l.moduleId === "rspace") || layers[0];
const sourceLayer = layers.find(l => l.moduleId === sourceModule);
if (currentLayer && sourceLayer) {
const flowId = `flow-auto-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
sync.addFlow({
id: flowId,
kind: flowKind,
sourceLayerId: sourceLayer.id,
targetLayerId: currentLayer.id,
targetShapeId: shape.id,
label: sourceModule + " feed",
strength: 0.5,
active: true,
});
}
// Also ensure source module has a layer (add if missing)
if (!sourceLayer) {
sync.addLayer({
id: "layer-" + sourceModule,
moduleId: sourceModule,
label: sourceModule,
order: layers.length,
color: "",
visible: true,
createdAt: Date.now(),
});
}
}
});
// Arrow connection mode
@ -2411,18 +2418,17 @@
document.getElementById("wb-pencil")?.addEventListener("click", () => setWbTool("pencil"));
document.getElementById("wb-sticky")?.addEventListener("click", () => {
// Create a sticky note as a markdown shape with yellow background
setWbTool(null);
const shape = newShape("folk-markdown", {
content: "# Sticky Note\n\nClick to edit..."
setPendingTool("folk-markdown", {
content: "# Sticky Note\n\nClick to edit...",
__postCreate: (shape) => {
shape.width = 200;
shape.height = 200;
shape.style.background = "#fef08a";
shape.style.borderRadius = "4px";
shape.style.boxShadow = "2px 2px 8px rgba(0,0,0,0.15)";
},
});
if (shape) {
shape.width = 200;
shape.height = 200;
shape.style.background = "#fef08a";
shape.style.borderRadius = "4px";
shape.style.boxShadow = "2px 2px 8px rgba(0,0,0,0.15)";
}
});
document.getElementById("wb-rect")?.addEventListener("click", () => setWbTool("rect"));
document.getElementById("wb-circle")?.addEventListener("click", () => setWbTool("circle"));
@ -2694,7 +2700,7 @@
if (!btn) return;
// Keep open for connect, memory, group toggles, collapse, whiteboard tools
const keepOpen = ["new-arrow", "toggle-memory", "toggle-theme", "zoom-in", "zoom-out", "reset-view", "toolbar-collapse",
"wb-pencil", "wb-sticky", "wb-rect", "wb-circle", "wb-line", "wb-eraser"];
"wb-pencil", "wb-rect", "wb-circle", "wb-line", "wb-eraser"];
if (btn.classList.contains("toolbar-group-toggle")) return;
if (!keepOpen.includes(btn.id)) {
toolbarEl.classList.remove("mobile-open");