|
|
|
|
@ -162,17 +162,30 @@
|
|
|
|
|
folk-transcription,
|
|
|
|
|
folk-video-chat,
|
|
|
|
|
folk-obs-note,
|
|
|
|
|
folk-workflow-block {
|
|
|
|
|
folk-workflow-block,
|
|
|
|
|
folk-itinerary,
|
|
|
|
|
folk-destination,
|
|
|
|
|
folk-budget,
|
|
|
|
|
folk-packing-list,
|
|
|
|
|
folk-booking {
|
|
|
|
|
position: absolute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.connect-mode folk-markdown,
|
|
|
|
|
.connect-mode folk-wrapper {
|
|
|
|
|
.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-transcription,
|
|
|
|
|
folk-video-chat, folk-obs-note, folk-workflow-block,
|
|
|
|
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
|
|
|
|
folk-booking) {
|
|
|
|
|
cursor: crosshair;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.connect-mode folk-markdown:hover,
|
|
|
|
|
.connect-mode folk-wrapper:hover {
|
|
|
|
|
.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-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-offset: 4px;
|
|
|
|
|
}
|
|
|
|
|
@ -196,6 +209,7 @@
|
|
|
|
|
<button id="add-chat" title="Add Chat">💬 Chat</button>
|
|
|
|
|
<button id="add-piano" title="Add Piano">🎹 Piano</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-map" title="Add Map">🗺️ Map</button>
|
|
|
|
|
<button id="add-image-gen" title="AI Image Generation">🎨 Image</button>
|
|
|
|
|
@ -293,6 +307,16 @@
|
|
|
|
|
const statusText = document.getElementById("status-text");
|
|
|
|
|
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
|
|
|
|
|
const sync = new CommunitySync(communitySlug);
|
|
|
|
|
|
|
|
|
|
@ -369,12 +393,19 @@
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isProcessingRemote = true;
|
|
|
|
|
const shape = createShapeElement(data);
|
|
|
|
|
setupShapeEventListeners(shape);
|
|
|
|
|
canvas.appendChild(shape);
|
|
|
|
|
sync.registerShape(shape);
|
|
|
|
|
isProcessingRemote = false;
|
|
|
|
|
try {
|
|
|
|
|
isProcessingRemote = true;
|
|
|
|
|
const shape = createShapeElement(data);
|
|
|
|
|
if (shape) {
|
|
|
|
|
setupShapeEventListeners(shape);
|
|
|
|
|
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
|
|
|
|
|
@ -558,317 +589,129 @@
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add markdown note button
|
|
|
|
|
document.getElementById("add-markdown").addEventListener("click", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const shape = document.createElement("folk-markdown");
|
|
|
|
|
shape.id = id;
|
|
|
|
|
shape.x = 100 + Math.random() * 200;
|
|
|
|
|
shape.y = 100 + Math.random() * 200;
|
|
|
|
|
shape.width = 300;
|
|
|
|
|
shape.height = 200;
|
|
|
|
|
shape.content = "# New Note\n\nStart typing...";
|
|
|
|
|
// Default dimensions for each shape type
|
|
|
|
|
const SHAPE_DEFAULTS = {
|
|
|
|
|
"folk-markdown": { width: 300, height: 200 },
|
|
|
|
|
"folk-wrapper": { width: 320, height: 240 },
|
|
|
|
|
"folk-slide": { width: 720, height: 480 },
|
|
|
|
|
"folk-chat": { width: 400, height: 500 },
|
|
|
|
|
"folk-google-item": { width: 280, height: 180 },
|
|
|
|
|
"folk-piano": { width: 800, height: 600 },
|
|
|
|
|
"folk-embed": { width: 480, height: 360 },
|
|
|
|
|
"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);
|
|
|
|
|
canvas.appendChild(shape);
|
|
|
|
|
sync.registerShape(shape);
|
|
|
|
|
// Get the center of the current viewport in canvas coordinates
|
|
|
|
|
function getViewportCenter() {
|
|
|
|
|
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", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
|
|
|
|
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
|
|
|
|
|
|
|
|
|
const shape = document.createElement("folk-wrapper");
|
|
|
|
|
shape.id = id;
|
|
|
|
|
shape.x = 100 + Math.random() * 200;
|
|
|
|
|
shape.y = 100 + Math.random() * 200;
|
|
|
|
|
shape.width = 320;
|
|
|
|
|
shape.height = 240;
|
|
|
|
|
shape.title = "New Card";
|
|
|
|
|
shape.icon = icons[Math.floor(Math.random() * icons.length)];
|
|
|
|
|
shape.primaryColor = colors[Math.floor(Math.random() * colors.length)];
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
const shape = createAndAddShape("folk-wrapper", {
|
|
|
|
|
title: "New Card",
|
|
|
|
|
icon: icons[Math.floor(Math.random() * icons.length)],
|
|
|
|
|
primaryColor: colors[Math.floor(Math.random() * colors.length)],
|
|
|
|
|
});
|
|
|
|
|
if (shape) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add slide button
|
|
|
|
|
document.getElementById("add-slide").addEventListener("click", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${++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);
|
|
|
|
|
createAndAddShape("folk-slide", { label: `Slide ${shapeCounter}` });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add chat button
|
|
|
|
|
document.getElementById("add-chat").addEventListener("click", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const shape = document.createElement("folk-chat");
|
|
|
|
|
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);
|
|
|
|
|
const id = `shape-${Date.now()}-${shapeCounter}`;
|
|
|
|
|
createAndAddShape("folk-chat", { roomId: `room-${id}` });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add piano button
|
|
|
|
|
document.getElementById("add-piano").addEventListener("click", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const shape = document.createElement("folk-piano");
|
|
|
|
|
shape.id = id;
|
|
|
|
|
shape.x = 100 + Math.random() * 200;
|
|
|
|
|
shape.y = 100 + Math.random() * 200;
|
|
|
|
|
shape.width = 800;
|
|
|
|
|
shape.height = 600;
|
|
|
|
|
|
|
|
|
|
setupShapeEventListeners(shape);
|
|
|
|
|
canvas.appendChild(shape);
|
|
|
|
|
sync.registerShape(shape);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
document.getElementById("add-piano").addEventListener("click", () => createAndAddShape("folk-piano"));
|
|
|
|
|
document.getElementById("add-embed").addEventListener("click", () => createAndAddShape("folk-embed"));
|
|
|
|
|
document.getElementById("add-calendar").addEventListener("click", () => createAndAddShape("folk-calendar"));
|
|
|
|
|
document.getElementById("add-map").addEventListener("click", () => createAndAddShape("folk-map"));
|
|
|
|
|
document.getElementById("add-image-gen").addEventListener("click", () => createAndAddShape("folk-image-gen"));
|
|
|
|
|
document.getElementById("add-video-gen").addEventListener("click", () => createAndAddShape("folk-video-gen"));
|
|
|
|
|
document.getElementById("add-prompt").addEventListener("click", () => createAndAddShape("folk-prompt"));
|
|
|
|
|
document.getElementById("add-transcription").addEventListener("click", () => createAndAddShape("folk-transcription"));
|
|
|
|
|
document.getElementById("add-video-chat").addEventListener("click", () => createAndAddShape("folk-video-chat"));
|
|
|
|
|
document.getElementById("add-obs-note").addEventListener("click", () => createAndAddShape("folk-obs-note"));
|
|
|
|
|
document.getElementById("add-workflow").addEventListener("click", () => createAndAddShape("folk-workflow-block"));
|
|
|
|
|
document.getElementById("add-google-item").addEventListener("click", () => {
|
|
|
|
|
createAndAddShape("folk-google-item", { service: "drive", title: "New Google Item" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Trip planning components
|
|
|
|
|
document.getElementById("add-itinerary").addEventListener("click", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const shape = document.createElement("folk-itinerary");
|
|
|
|
|
shape.id = id;
|
|
|
|
|
shape.x = 100 + Math.random() * 200;
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
document.getElementById("add-itinerary").addEventListener("click", () => createAndAddShape("folk-itinerary"));
|
|
|
|
|
document.getElementById("add-destination").addEventListener("click", () => createAndAddShape("folk-destination"));
|
|
|
|
|
document.getElementById("add-budget").addEventListener("click", () => createAndAddShape("folk-budget"));
|
|
|
|
|
document.getElementById("add-packing-list").addEventListener("click", () => createAndAddShape("folk-packing-list"));
|
|
|
|
|
document.getElementById("add-booking").addEventListener("click", () => createAndAddShape("folk-booking"));
|
|
|
|
|
|
|
|
|
|
// Arrow connection mode
|
|
|
|
|
let connectMode = false;
|
|
|
|
|
@ -890,7 +733,7 @@
|
|
|
|
|
canvas.addEventListener("click", (e) => {
|
|
|
|
|
if (!connectMode) return;
|
|
|
|
|
|
|
|
|
|
const target = e.target.closest("folk-markdown, folk-wrapper");
|
|
|
|
|
const target = e.target.closest(CONNECTABLE_SELECTOR);
|
|
|
|
|
if (!target || !target.id) return;
|
|
|
|
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
@ -1014,10 +857,12 @@
|
|
|
|
|
updateCanvasTransform();
|
|
|
|
|
}, { passive: false });
|
|
|
|
|
|
|
|
|
|
// Keep-alive ping
|
|
|
|
|
// Keep-alive ping to prevent WebSocket idle timeout
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
if (sync.doc) {
|
|
|
|
|
// Sync is connected, nothing to do
|
|
|
|
|
try {
|
|
|
|
|
sync.ping();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("[Canvas] Keep-alive ping failed:", e);
|
|
|
|
|
}
|
|
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
|
|