321 lines
7.7 KiB
HTML
321 lines
7.7 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 {
|
|
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">+ Add Note</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, CommunitySync } from "@lib";
|
|
|
|
// Register custom elements
|
|
FolkShape.define();
|
|
FolkMarkdown.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-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);
|
|
});
|
|
|
|
// 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>
|