diff --git a/website/canvas.html b/website/canvas.html index 618af72..443d31e 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -982,29 +982,6 @@ background: var(--rs-bg-hover, rgba(255,255,255,0.08)); } - #people-conn-status { - display: none; - align-items: center; - gap: 4px; - font-size: 11px; - padding-left: 6px; - border-left: 1px solid var(--rs-border, rgba(255,255,255,0.1)); - } - - #people-conn-status.visible { - display: inline-flex; - } - - #people-conn-status .conn-dot { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; - } - - #people-conn-status .conn-dot.pulse { - animation: pulse 1.2s ease-in-out infinite; - } #people-dots { display: flex; @@ -2000,7 +1977,6 @@
1 online -
@@ -3392,7 +3368,6 @@ const peopleBadgeText = document.getElementById("people-badge-text"); const peopleCount = document.getElementById("people-count"); const peopleList = document.getElementById("people-list"); - const peopleConnStatus = document.getElementById("people-conn-status"); let connState = "connecting"; // "connected" | "offline" | "reconnecting" | "connecting" const pingToast = document.getElementById("ping-toast"); const pingToastText = document.getElementById("ping-toast-text"); @@ -3439,15 +3414,26 @@ function renderPeopleBadge() { const totalCount = onlinePeers.size + 1; // +1 for self peopleDots.innerHTML = ""; - if (!isMultiplayer) { - // Offline / solo mode + + if (connState === "offline") { + // Actually disconnected from internet peopleBadgeText.textContent = "Offline"; + peopleBadge.title = "You\u2019re offline \u2014 your changes are saved locally and will resync when you reconnect to the internet."; const selfDot = document.createElement("span"); selfDot.className = "dot"; - selfDot.style.background = "#64748b"; + selfDot.style.background = "#f59e0b"; + peopleDots.appendChild(selfDot); + } else if (connState === "reconnecting" || connState === "connecting") { + peopleBadgeText.textContent = "Reconnecting\u2026"; + peopleBadge.title = "Reconnecting to the server \u2014 your changes are saved locally and will resync automatically."; + const selfDot = document.createElement("span"); + selfDot.className = "dot"; + selfDot.style.background = "#3b82f6"; peopleDots.appendChild(selfDot); } else { + // Connected peopleBadgeText.textContent = totalCount === 1 ? "1 online" : `${totalCount} online`; + peopleBadge.title = ""; // Self dot const selfDot = document.createElement("span"); selfDot.className = "dot"; @@ -3464,30 +3450,28 @@ dotCount++; } } - peopleCount.textContent = isMultiplayer ? totalCount : "—"; - // Connection status indicator - if (connState === "connected" || !isMultiplayer) { - peopleConnStatus.classList.remove("visible"); - peopleConnStatus.innerHTML = ""; - } else { - const color = connState === "offline" ? "#f59e0b" : "#3b82f6"; - const label = connState === "offline" ? "Offline" : "Reconnecting…"; - const pulse = connState !== "offline" ? " pulse" : ""; - peopleConnStatus.innerHTML = `${label}`; - peopleConnStatus.classList.add("visible"); - } + peopleCount.textContent = connState === "connected" ? totalCount : "\u2014"; } function renderPeoplePanel() { peopleList.innerHTML = ""; - // Self row with online/offline toggle + // Show offline notice if disconnected + if (connState === "offline" || connState === "reconnecting" || connState === "connecting") { + const notice = document.createElement("div"); + notice.style.cssText = "padding:8px 16px;font-size:12px;color:var(--rs-text-muted);background:var(--rs-bg-surface-raised,rgba(255,255,255,0.04));border-bottom:1px solid var(--rs-border-subtle,rgba(255,255,255,0.06))"; + notice.textContent = connState === "offline" + ? "\u26a0 You\u2019re offline. Changes are saved locally and will resync when you reconnect." + : "Reconnecting to server\u2026"; + peopleList.appendChild(notice); + } + // Self row with cursor visibility toggle const selfRow = document.createElement("div"); selfRow.className = "people-row"; - selfRow.innerHTML = ` + selfRow.innerHTML = ` ${escapeHtml(storedUsername)} (you) - - - + + + `; selfRow.querySelector(".mode-solo").addEventListener("click", () => setMultiplayerMode(false)); selfRow.querySelector(".mode-multi").addEventListener("click", () => setMultiplayerMode(true)); @@ -6628,10 +6612,18 @@ updateCanvasTransform(); }, { passive: false }); + // ── Shadow-DOM-aware text input check ── + // e.target is retargeted to the shadow host, so we must walk composedPath() + function isInTextInput(e) { + return e.composedPath().some(el => + el instanceof HTMLElement && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) + ); + } + // ── Space key tracking for space+drag pan ── let spaceHeld = false; document.addEventListener("keydown", (e) => { - if (e.code === "Space" && !e.target.closest("input, textarea, [contenteditable]")) { + if (e.code === "Space" && !isInTextInput(e)) { e.preventDefault(); spaceHeld = true; canvas.style.cursor = "grab"; @@ -6663,7 +6655,7 @@ document.addEventListener("keydown", (e) => { if ((e.key === "Delete" || e.key === "Backspace") && - !e.target.closest("input, textarea, [contenteditable]") && + !isInTextInput(e) && !bulkDeleteOverlay && selectedShapeIds.size > 0) { if (selectedShapeIds.size > 5) { @@ -6677,7 +6669,7 @@ // ── Undo / Redo (Ctrl+Z / Ctrl+Shift+Z) ── document.addEventListener("keydown", (e) => { if ((e.key === "z" || e.key === "Z") && (e.ctrlKey || e.metaKey) && - !e.target.closest("input, textarea, [contenteditable]")) { + !isInTextInput(e)) { e.preventDefault(); if (e.shiftKey) { sync.redo();