fix: overhaul canvas shape creation, connections, and sync
- Fix CSS position:absolute missing for 5 trip planning shapes - Expand arrow connection mode to all 21 shape types (was only 2) - Center new shapes in viewport instead of clustering top-left - Extract createAndAddShape() utility, eliminating ~270 lines of duplication - Add missing Google Item toolbar button - Add error handling on remote shape creation (try-catch-finally) - Implement actual WebSocket keep-alive ping (was a no-op) - Use shape.toJSON() in sync layer to capture all shape properties (was only 3 types) - Add index signature to ShapeData for arbitrary shape-specific properties Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
eedc6b1b4a
commit
38636862d8
|
|
@ -23,6 +23,8 @@ export interface ShapeData {
|
||||||
isMinimized?: boolean;
|
isMinimized?: boolean;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
// Allow arbitrary shape-specific properties from toJSON()
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automerge document structure
|
// Automerge document structure
|
||||||
|
|
@ -255,6 +257,13 @@ export class CommunitySync extends EventTarget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a keep-alive ping to prevent WebSocket idle timeout
|
||||||
|
*/
|
||||||
|
ping(): void {
|
||||||
|
this.#send({ type: "ping" });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a shape element for syncing
|
* Register a shape element for syncing
|
||||||
*/
|
*/
|
||||||
|
|
@ -309,42 +318,27 @@ export class CommunitySync extends EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert FolkShape to serializable data
|
* Convert FolkShape to serializable data using the shape's own toJSON() method.
|
||||||
|
* This ensures all shape-specific properties are captured for every shape type.
|
||||||
*/
|
*/
|
||||||
#shapeToData(shape: FolkShape): ShapeData {
|
#shapeToData(shape: FolkShape): ShapeData {
|
||||||
|
const json = (shape as any).toJSON?.() ?? {};
|
||||||
|
|
||||||
const data: ShapeData = {
|
const data: ShapeData = {
|
||||||
type: shape.tagName.toLowerCase(),
|
type: (json.type as string) || shape.tagName.toLowerCase(),
|
||||||
id: shape.id,
|
id: (json.id as string) || shape.id,
|
||||||
x: shape.x,
|
x: (json.x as number) ?? shape.x,
|
||||||
y: shape.y,
|
y: (json.y as number) ?? shape.y,
|
||||||
width: shape.width,
|
width: (json.width as number) ?? shape.width,
|
||||||
height: shape.height,
|
height: (json.height as number) ?? shape.height,
|
||||||
rotation: shape.rotation,
|
rotation: (json.rotation as number) ?? shape.rotation,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add content for markdown shapes
|
// Merge all extra properties from toJSON
|
||||||
if ("content" in shape && typeof (shape as any).content === "string") {
|
for (const [key, value] of Object.entries(json)) {
|
||||||
data.content = (shape as any).content;
|
if (!(key in data)) {
|
||||||
}
|
data[key] = value;
|
||||||
|
}
|
||||||
// Add arrow properties
|
|
||||||
if (shape.tagName.toLowerCase() === "folk-arrow") {
|
|
||||||
const arrow = shape as any;
|
|
||||||
if (arrow.sourceId) data.sourceId = arrow.sourceId;
|
|
||||||
if (arrow.targetId) data.targetId = arrow.targetId;
|
|
||||||
if (arrow.color) data.color = arrow.color;
|
|
||||||
if (arrow.strokeWidth) data.strokeWidth = arrow.strokeWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add wrapper properties
|
|
||||||
if (shape.tagName.toLowerCase() === "folk-wrapper") {
|
|
||||||
const wrapper = shape as any;
|
|
||||||
if (wrapper.title) data.title = wrapper.title;
|
|
||||||
if (wrapper.icon) data.icon = wrapper.icon;
|
|
||||||
if (wrapper.primaryColor) data.primaryColor = wrapper.primaryColor;
|
|
||||||
if (wrapper.isMinimized !== undefined) data.isMinimized = wrapper.isMinimized;
|
|
||||||
if (wrapper.isPinned !== undefined) data.isPinned = wrapper.isPinned;
|
|
||||||
if (wrapper.tags?.length) data.tags = wrapper.tags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|
|
||||||
|
|
@ -162,17 +162,30 @@
|
||||||
folk-transcription,
|
folk-transcription,
|
||||||
folk-video-chat,
|
folk-video-chat,
|
||||||
folk-obs-note,
|
folk-obs-note,
|
||||||
folk-workflow-block {
|
folk-workflow-block,
|
||||||
|
folk-itinerary,
|
||||||
|
folk-destination,
|
||||||
|
folk-budget,
|
||||||
|
folk-packing-list,
|
||||||
|
folk-booking {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-mode folk-markdown,
|
.connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat,
|
||||||
.connect-mode folk-wrapper {
|
folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map,
|
||||||
|
folk-image-gen, folk-video-gen, folk-prompt, folk-transcription,
|
||||||
|
folk-video-chat, folk-obs-note, folk-workflow-block,
|
||||||
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||||
|
folk-booking) {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-mode folk-markdown:hover,
|
.connect-mode :is(folk-markdown, folk-wrapper, folk-slide, folk-chat,
|
||||||
.connect-mode folk-wrapper:hover {
|
folk-google-item, folk-piano, folk-embed, folk-calendar, folk-map,
|
||||||
|
folk-image-gen, folk-video-gen, folk-prompt, folk-transcription,
|
||||||
|
folk-video-chat, folk-obs-note, folk-workflow-block,
|
||||||
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||||
|
folk-booking):hover {
|
||||||
outline: 2px dashed #3b82f6;
|
outline: 2px dashed #3b82f6;
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -196,6 +209,7 @@
|
||||||
<button id="add-chat" title="Add Chat">💬 Chat</button>
|
<button id="add-chat" title="Add Chat">💬 Chat</button>
|
||||||
<button id="add-piano" title="Add Piano">🎹 Piano</button>
|
<button id="add-piano" title="Add Piano">🎹 Piano</button>
|
||||||
<button id="add-embed" title="Add Web Embed">🔗 Embed</button>
|
<button id="add-embed" title="Add Web Embed">🔗 Embed</button>
|
||||||
|
<button id="add-google-item" title="Add Google Item">📎 Google</button>
|
||||||
<button id="add-calendar" title="Add Calendar">📅 Calendar</button>
|
<button id="add-calendar" title="Add Calendar">📅 Calendar</button>
|
||||||
<button id="add-map" title="Add Map">🗺️ Map</button>
|
<button id="add-map" title="Add Map">🗺️ Map</button>
|
||||||
<button id="add-image-gen" title="AI Image Generation">🎨 Image</button>
|
<button id="add-image-gen" title="AI Image Generation">🎨 Image</button>
|
||||||
|
|
@ -293,6 +307,16 @@
|
||||||
const statusText = document.getElementById("status-text");
|
const statusText = document.getElementById("status-text");
|
||||||
let shapeCounter = 0;
|
let shapeCounter = 0;
|
||||||
|
|
||||||
|
// All shape tag names that can be arrow connection endpoints
|
||||||
|
const CONNECTABLE_SELECTOR = [
|
||||||
|
"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-transcription", "folk-video-chat", "folk-obs-note",
|
||||||
|
"folk-workflow-block", "folk-itinerary", "folk-destination",
|
||||||
|
"folk-budget", "folk-packing-list", "folk-booking"
|
||||||
|
].join(", ");
|
||||||
|
|
||||||
// Initialize CommunitySync
|
// Initialize CommunitySync
|
||||||
const sync = new CommunitySync(communitySlug);
|
const sync = new CommunitySync(communitySlug);
|
||||||
|
|
||||||
|
|
@ -369,12 +393,19 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isProcessingRemote = true;
|
try {
|
||||||
const shape = createShapeElement(data);
|
isProcessingRemote = true;
|
||||||
setupShapeEventListeners(shape);
|
const shape = createShapeElement(data);
|
||||||
canvas.appendChild(shape);
|
if (shape) {
|
||||||
sync.registerShape(shape);
|
setupShapeEventListeners(shape);
|
||||||
isProcessingRemote = false;
|
canvas.appendChild(shape);
|
||||||
|
sync.registerShape(shape);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[Canvas] Failed to create remote shape ${data.id} (${data.type}):`, err);
|
||||||
|
} finally {
|
||||||
|
isProcessingRemote = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle shape deletion from remote
|
// Handle shape deletion from remote
|
||||||
|
|
@ -558,317 +589,129 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add markdown note button
|
// Default dimensions for each shape type
|
||||||
document.getElementById("add-markdown").addEventListener("click", () => {
|
const SHAPE_DEFAULTS = {
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
"folk-markdown": { width: 300, height: 200 },
|
||||||
const shape = document.createElement("folk-markdown");
|
"folk-wrapper": { width: 320, height: 240 },
|
||||||
shape.id = id;
|
"folk-slide": { width: 720, height: 480 },
|
||||||
shape.x = 100 + Math.random() * 200;
|
"folk-chat": { width: 400, height: 500 },
|
||||||
shape.y = 100 + Math.random() * 200;
|
"folk-google-item": { width: 280, height: 180 },
|
||||||
shape.width = 300;
|
"folk-piano": { width: 800, height: 600 },
|
||||||
shape.height = 200;
|
"folk-embed": { width: 480, height: 360 },
|
||||||
shape.content = "# New Note\n\nStart typing...";
|
"folk-calendar": { width: 320, height: 380 },
|
||||||
|
"folk-map": { width: 500, height: 400 },
|
||||||
|
"folk-image-gen": { width: 400, height: 500 },
|
||||||
|
"folk-video-gen": { width: 450, height: 550 },
|
||||||
|
"folk-prompt": { width: 450, height: 500 },
|
||||||
|
"folk-transcription": { width: 400, height: 450 },
|
||||||
|
"folk-video-chat": { width: 480, height: 400 },
|
||||||
|
"folk-obs-note": { width: 450, height: 500 },
|
||||||
|
"folk-workflow-block": { width: 240, height: 180 },
|
||||||
|
"folk-itinerary": { width: 320, height: 400 },
|
||||||
|
"folk-destination": { width: 280, height: 220 },
|
||||||
|
"folk-budget": { width: 300, height: 350 },
|
||||||
|
"folk-packing-list": { width: 280, height: 350 },
|
||||||
|
"folk-booking": { width: 300, height: 240 },
|
||||||
|
};
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
// Get the center of the current viewport in canvas coordinates
|
||||||
canvas.appendChild(shape);
|
function getViewportCenter() {
|
||||||
sync.registerShape(shape);
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const viewCenterX = rect.width / 2;
|
||||||
|
const viewCenterY = rect.height / 2;
|
||||||
|
// Reverse the canvas transform to get canvas coordinates
|
||||||
|
const canvasX = (viewCenterX - panX) / scale;
|
||||||
|
const canvasY = (viewCenterY - panY) / scale;
|
||||||
|
// Add jitter so shapes don't stack perfectly
|
||||||
|
return {
|
||||||
|
x: canvasX + (Math.random() - 0.5) * 40,
|
||||||
|
y: canvasY + (Math.random() - 0.5) * 40
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a shape, position it at viewport center, add to canvas, and register for sync
|
||||||
|
function createAndAddShape(tagName, props = {}) {
|
||||||
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||||
|
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
|
||||||
|
|
||||||
|
const shape = document.createElement(tagName);
|
||||||
|
shape.id = id;
|
||||||
|
|
||||||
|
const center = getViewportCenter();
|
||||||
|
shape.x = center.x - defaults.width / 2;
|
||||||
|
shape.y = center.y - defaults.height / 2;
|
||||||
|
shape.width = defaults.width;
|
||||||
|
shape.height = defaults.height;
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(props)) {
|
||||||
|
shape[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setupShapeEventListeners(shape);
|
||||||
|
canvas.appendChild(shape);
|
||||||
|
sync.registerShape(shape);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Canvas] Failed to create shape ${tagName}:`, e);
|
||||||
|
shape.remove?.();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toolbar button handlers
|
||||||
|
document.getElementById("add-markdown").addEventListener("click", () => {
|
||||||
|
createAndAddShape("folk-markdown", { content: "# New Note\n\nStart typing..." });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add wrapper card button
|
|
||||||
document.getElementById("add-wrapper").addEventListener("click", () => {
|
document.getElementById("add-wrapper").addEventListener("click", () => {
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
||||||
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
||||||
|
const shape = createAndAddShape("folk-wrapper", {
|
||||||
const shape = document.createElement("folk-wrapper");
|
title: "New Card",
|
||||||
shape.id = id;
|
icon: icons[Math.floor(Math.random() * icons.length)],
|
||||||
shape.x = 100 + Math.random() * 200;
|
primaryColor: colors[Math.floor(Math.random() * colors.length)],
|
||||||
shape.y = 100 + Math.random() * 200;
|
});
|
||||||
shape.width = 320;
|
if (shape) {
|
||||||
shape.height = 240;
|
const content = document.createElement("div");
|
||||||
shape.title = "New Card";
|
content.style.padding = "16px";
|
||||||
shape.icon = icons[Math.floor(Math.random() * icons.length)];
|
content.style.color = "#374151";
|
||||||
shape.primaryColor = colors[Math.floor(Math.random() * colors.length)];
|
content.innerHTML = "<p>Click to edit this card...</p>";
|
||||||
|
shape.appendChild(content);
|
||||||
// Add some placeholder content inside the wrapper
|
}
|
||||||
const content = document.createElement("div");
|
|
||||||
content.style.padding = "16px";
|
|
||||||
content.style.color = "#374151";
|
|
||||||
content.innerHTML = "<p>Click to edit this card...</p>";
|
|
||||||
shape.appendChild(content);
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add slide button
|
|
||||||
document.getElementById("add-slide").addEventListener("click", () => {
|
document.getElementById("add-slide").addEventListener("click", () => {
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
createAndAddShape("folk-slide", { label: `Slide ${shapeCounter}` });
|
||||||
const shape = document.createElement("folk-slide");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 720;
|
|
||||||
shape.height = 480;
|
|
||||||
shape.label = `Slide ${shapeCounter}`;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add chat button
|
|
||||||
document.getElementById("add-chat").addEventListener("click", () => {
|
document.getElementById("add-chat").addEventListener("click", () => {
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
const id = `shape-${Date.now()}-${shapeCounter}`;
|
||||||
const shape = document.createElement("folk-chat");
|
createAndAddShape("folk-chat", { roomId: `room-${id}` });
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 400;
|
|
||||||
shape.height = 500;
|
|
||||||
shape.roomId = `room-${id}`;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add piano button
|
document.getElementById("add-piano").addEventListener("click", () => createAndAddShape("folk-piano"));
|
||||||
document.getElementById("add-piano").addEventListener("click", () => {
|
document.getElementById("add-embed").addEventListener("click", () => createAndAddShape("folk-embed"));
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
document.getElementById("add-calendar").addEventListener("click", () => createAndAddShape("folk-calendar"));
|
||||||
const shape = document.createElement("folk-piano");
|
document.getElementById("add-map").addEventListener("click", () => createAndAddShape("folk-map"));
|
||||||
shape.id = id;
|
document.getElementById("add-image-gen").addEventListener("click", () => createAndAddShape("folk-image-gen"));
|
||||||
shape.x = 100 + Math.random() * 200;
|
document.getElementById("add-video-gen").addEventListener("click", () => createAndAddShape("folk-video-gen"));
|
||||||
shape.y = 100 + Math.random() * 200;
|
document.getElementById("add-prompt").addEventListener("click", () => createAndAddShape("folk-prompt"));
|
||||||
shape.width = 800;
|
document.getElementById("add-transcription").addEventListener("click", () => createAndAddShape("folk-transcription"));
|
||||||
shape.height = 600;
|
document.getElementById("add-video-chat").addEventListener("click", () => createAndAddShape("folk-video-chat"));
|
||||||
|
document.getElementById("add-obs-note").addEventListener("click", () => createAndAddShape("folk-obs-note"));
|
||||||
setupShapeEventListeners(shape);
|
document.getElementById("add-workflow").addEventListener("click", () => createAndAddShape("folk-workflow-block"));
|
||||||
canvas.appendChild(shape);
|
document.getElementById("add-google-item").addEventListener("click", () => {
|
||||||
sync.registerShape(shape);
|
createAndAddShape("folk-google-item", { service: "drive", title: "New Google Item" });
|
||||||
});
|
|
||||||
|
|
||||||
// Add embed button
|
|
||||||
document.getElementById("add-embed").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-embed");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 480;
|
|
||||||
shape.height = 360;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add calendar button
|
|
||||||
document.getElementById("add-calendar").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-calendar");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 320;
|
|
||||||
shape.height = 380;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add map button
|
|
||||||
document.getElementById("add-map").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-map");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 500;
|
|
||||||
shape.height = 400;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add image gen button
|
|
||||||
document.getElementById("add-image-gen").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-image-gen");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 400;
|
|
||||||
shape.height = 500;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add video gen button
|
|
||||||
document.getElementById("add-video-gen").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-video-gen");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 450;
|
|
||||||
shape.height = 550;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add prompt button
|
|
||||||
document.getElementById("add-prompt").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-prompt");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 450;
|
|
||||||
shape.height = 500;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add transcription button
|
|
||||||
document.getElementById("add-transcription").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-transcription");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 400;
|
|
||||||
shape.height = 450;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add video chat button
|
|
||||||
document.getElementById("add-video-chat").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-video-chat");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 480;
|
|
||||||
shape.height = 400;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add rich note button
|
|
||||||
document.getElementById("add-obs-note").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-obs-note");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 450;
|
|
||||||
shape.height = 500;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add workflow block button
|
|
||||||
document.getElementById("add-workflow").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-workflow-block");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 240;
|
|
||||||
shape.height = 180;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trip planning components
|
// Trip planning components
|
||||||
document.getElementById("add-itinerary").addEventListener("click", () => {
|
document.getElementById("add-itinerary").addEventListener("click", () => createAndAddShape("folk-itinerary"));
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
document.getElementById("add-destination").addEventListener("click", () => createAndAddShape("folk-destination"));
|
||||||
const shape = document.createElement("folk-itinerary");
|
document.getElementById("add-budget").addEventListener("click", () => createAndAddShape("folk-budget"));
|
||||||
shape.id = id;
|
document.getElementById("add-packing-list").addEventListener("click", () => createAndAddShape("folk-packing-list"));
|
||||||
shape.x = 100 + Math.random() * 200;
|
document.getElementById("add-booking").addEventListener("click", () => createAndAddShape("folk-booking"));
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 320;
|
|
||||||
shape.height = 400;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("add-destination").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-destination");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 280;
|
|
||||||
shape.height = 220;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("add-budget").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-budget");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 300;
|
|
||||||
shape.height = 350;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("add-packing-list").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-packing-list");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 280;
|
|
||||||
shape.height = 350;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("add-booking").addEventListener("click", () => {
|
|
||||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
||||||
const shape = document.createElement("folk-booking");
|
|
||||||
shape.id = id;
|
|
||||||
shape.x = 100 + Math.random() * 200;
|
|
||||||
shape.y = 100 + Math.random() * 200;
|
|
||||||
shape.width = 300;
|
|
||||||
shape.height = 240;
|
|
||||||
|
|
||||||
setupShapeEventListeners(shape);
|
|
||||||
canvas.appendChild(shape);
|
|
||||||
sync.registerShape(shape);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Arrow connection mode
|
// Arrow connection mode
|
||||||
let connectMode = false;
|
let connectMode = false;
|
||||||
|
|
@ -890,7 +733,7 @@
|
||||||
canvas.addEventListener("click", (e) => {
|
canvas.addEventListener("click", (e) => {
|
||||||
if (!connectMode) return;
|
if (!connectMode) return;
|
||||||
|
|
||||||
const target = e.target.closest("folk-markdown, folk-wrapper");
|
const target = e.target.closest(CONNECTABLE_SELECTOR);
|
||||||
if (!target || !target.id) return;
|
if (!target || !target.id) return;
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -1014,10 +857,12 @@
|
||||||
updateCanvasTransform();
|
updateCanvasTransform();
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
// Keep-alive ping
|
// Keep-alive ping to prevent WebSocket idle timeout
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (sync.doc) {
|
try {
|
||||||
// Sync is connected, nothing to do
|
sync.ping();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Canvas] Keep-alive ping failed:", e);
|
||||||
}
|
}
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue