rspace-online/website/canvas.html

361 lines
9.3 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>rSpace Canvas</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
#toolbar {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
padding: 8px 16px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
#toolbar button {
padding: 8px 16px;
border: none;
border-radius: 8px;
background: #f1f5f9;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
#toolbar button:hover {
background: #e2e8f0;
}
#toolbar button.active {
background: #14b8a6;
color: white;
}
#community-info {
position: fixed;
top: 16px;
left: 16px;
padding: 8px 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
#community-info h2 {
font-size: 14px;
color: #0f172a;
}
#community-info p {
font-size: 12px;
color: #64748b;
}
#status {
position: fixed;
bottom: 16px;
right: 16px;
padding: 8px 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
font-size: 12px;
color: #64748b;
z-index: 1000;
display: flex;
align-items: center;
gap: 8px;
}
#status .indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #64748b;
}
#status.connected .indicator {
background: #22c55e;
}
#status.disconnected .indicator {
background: #ef4444;
}
#status.syncing .indicator {
background: #f59e0b;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
#canvas {
width: 100%;
height: 100%;
background: linear-gradient(#f1f5f9 1px, transparent 1px),
linear-gradient(90deg, #f1f5f9 1px, transparent 1px);
background-size: 20px 20px;
background-position: -1px -1px;
position: relative;
overflow: hidden;
}
folk-markdown,
folk-wrapper {
position: absolute;
}
</style>
</head>
<body>
<div id="community-info">
<h2 id="community-name">Loading...</h2>
<p id="community-slug"></p>
</div>
<div id="toolbar">
<button id="add-markdown" title="Add Markdown Note">📝 Note</button>
<button id="add-wrapper" title="Add Wrapped Card">🗂️ Card</button>
<button id="zoom-in" title="Zoom In">+</button>
<button id="zoom-out" title="Zoom Out">-</button>
<button id="reset-view" title="Reset View">Reset</button>
</div>
<div id="status" class="disconnected">
<span class="indicator"></span>
<span id="status-text">Connecting...</span>
</div>
<div id="canvas"></div>
<script type="module">
import { FolkShape, FolkMarkdown, FolkWrapper, CommunitySync } from "@lib";
// Register custom elements
FolkShape.define();
FolkMarkdown.define();
FolkWrapper.define();
// Get community info from URL
const hostname = window.location.hostname;
const subdomain = hostname.split(".")[0];
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1";
const communitySlug = isLocalhost ? "demo" : subdomain;
// Update UI
document.getElementById("community-name").textContent = communitySlug;
document.getElementById("community-slug").textContent = `${communitySlug}.rspace.online`;
const canvas = document.getElementById("canvas");
const status = document.getElementById("status");
const statusText = document.getElementById("status-text");
let shapeCounter = 0;
// Initialize CommunitySync
const sync = new CommunitySync(communitySlug);
// Track if we're processing remote changes to avoid feedback loops
let isProcessingRemote = false;
// Handle connection status
sync.addEventListener("connected", () => {
status.className = "connected";
statusText.textContent = "Connected";
});
sync.addEventListener("disconnected", () => {
status.className = "disconnected";
statusText.textContent = "Reconnecting...";
});
sync.addEventListener("synced", (e) => {
console.log("[Canvas] Initial sync complete:", e.detail.shapes);
});
// Handle shape creation from remote
sync.addEventListener("create-shape", (e) => {
const data = e.detail;
// Check if shape already exists
if (document.getElementById(data.id)) {
return;
}
isProcessingRemote = true;
const shape = createShapeElement(data);
setupShapeEventListeners(shape);
canvas.appendChild(shape);
sync.registerShape(shape);
isProcessingRemote = false;
});
// Handle shape deletion from remote
sync.addEventListener("shape-deleted", (e) => {
const { shapeId, shape } = e.detail;
if (shape && shape.parentNode) {
shape.remove();
}
});
// Create a shape element from data
function createShapeElement(data) {
let shape;
switch (data.type) {
case "folk-wrapper":
shape = document.createElement("folk-wrapper");
if (data.title) shape.title = data.title;
if (data.icon) shape.icon = data.icon;
if (data.primaryColor) shape.primaryColor = data.primaryColor;
if (data.isMinimized) shape.isMinimized = data.isMinimized;
if (data.isPinned) shape.isPinned = data.isPinned;
if (data.tags) shape.tags = data.tags;
break;
case "folk-markdown":
default:
shape = document.createElement("folk-markdown");
if (data.content) shape.content = data.content;
break;
}
shape.id = data.id;
shape.x = data.x || 100;
shape.y = data.y || 100;
shape.width = data.width || 300;
shape.height = data.height || 200;
if (data.rotation) shape.rotation = data.rotation;
return shape;
}
// Setup event listeners for shape
function setupShapeEventListeners(shape) {
// Transform events (move, resize, rotate)
shape.addEventListener("folk-transform", (e) => {
if (!isProcessingRemote) {
// Already handled by CommunitySync registration
}
});
// Content change events (for markdown)
shape.addEventListener("content-change", (e) => {
if (!isProcessingRemote) {
// Already handled by CommunitySync registration
}
});
// Close button
shape.addEventListener("close", () => {
sync.deleteShape(shape.id);
shape.remove();
});
}
// 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...";
setupShapeEventListeners(shape);
canvas.appendChild(shape);
sync.registerShape(shape);
});
// 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);
});
// Zoom controls
let scale = 1;
const minScale = 0.25;
const maxScale = 4;
document.getElementById("zoom-in").addEventListener("click", () => {
scale = Math.min(scale * 1.25, maxScale);
canvas.style.transform = `scale(${scale})`;
canvas.style.transformOrigin = "center center";
});
document.getElementById("zoom-out").addEventListener("click", () => {
scale = Math.max(scale / 1.25, minScale);
canvas.style.transform = `scale(${scale})`;
canvas.style.transformOrigin = "center center";
});
document.getElementById("reset-view").addEventListener("click", () => {
scale = 1;
canvas.style.transform = "scale(1)";
});
// Keep-alive ping
setInterval(() => {
if (sync.doc) {
// Sync is connected, nothing to do
}
}, 30000);
// Connect to server
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws/${communitySlug}`;
sync.connect(wsUrl);
// Debug: expose sync for console inspection
window.sync = sync;
</script>
</body>
</html>