fix(ux): show offline only on actual disconnection, add resync tooltip
- Badge now reflects real connection state: "N online", "Reconnecting…", or "Offline" - Removed manual online/offline toggle (was confusing — showed both states) - Panel Solo/Share toggle remains for cursor visibility preference - Hover tooltip on badge when offline: "changes saved locally, will resync" - Panel shows offline/reconnecting notice banner when disconnected - Removed unused #people-conn-status element and CSS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
023a9e7fbd
commit
83fa874147
|
|
@ -982,29 +982,6 @@
|
||||||
background: var(--rs-bg-hover, rgba(255,255,255,0.08));
|
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 {
|
#people-dots {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -2000,7 +1977,6 @@
|
||||||
<div id="people-online-badge">
|
<div id="people-online-badge">
|
||||||
<span class="dots" id="people-dots"></span>
|
<span class="dots" id="people-dots"></span>
|
||||||
<span id="people-badge-text">1 online</span>
|
<span id="people-badge-text">1 online</span>
|
||||||
<span id="people-conn-status"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="people-panel">
|
<div id="people-panel">
|
||||||
<div id="people-panel-header">
|
<div id="people-panel-header">
|
||||||
|
|
@ -3392,7 +3368,6 @@
|
||||||
const peopleBadgeText = document.getElementById("people-badge-text");
|
const peopleBadgeText = document.getElementById("people-badge-text");
|
||||||
const peopleCount = document.getElementById("people-count");
|
const peopleCount = document.getElementById("people-count");
|
||||||
const peopleList = document.getElementById("people-list");
|
const peopleList = document.getElementById("people-list");
|
||||||
const peopleConnStatus = document.getElementById("people-conn-status");
|
|
||||||
let connState = "connecting"; // "connected" | "offline" | "reconnecting" | "connecting"
|
let connState = "connecting"; // "connected" | "offline" | "reconnecting" | "connecting"
|
||||||
const pingToast = document.getElementById("ping-toast");
|
const pingToast = document.getElementById("ping-toast");
|
||||||
const pingToastText = document.getElementById("ping-toast-text");
|
const pingToastText = document.getElementById("ping-toast-text");
|
||||||
|
|
@ -3439,15 +3414,26 @@
|
||||||
function renderPeopleBadge() {
|
function renderPeopleBadge() {
|
||||||
const totalCount = onlinePeers.size + 1; // +1 for self
|
const totalCount = onlinePeers.size + 1; // +1 for self
|
||||||
peopleDots.innerHTML = "";
|
peopleDots.innerHTML = "";
|
||||||
if (!isMultiplayer) {
|
|
||||||
// Offline / solo mode
|
if (connState === "offline") {
|
||||||
|
// Actually disconnected from internet
|
||||||
peopleBadgeText.textContent = "Offline";
|
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");
|
const selfDot = document.createElement("span");
|
||||||
selfDot.className = "dot";
|
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);
|
peopleDots.appendChild(selfDot);
|
||||||
} else {
|
} else {
|
||||||
|
// Connected
|
||||||
peopleBadgeText.textContent = totalCount === 1 ? "1 online" : `${totalCount} online`;
|
peopleBadgeText.textContent = totalCount === 1 ? "1 online" : `${totalCount} online`;
|
||||||
|
peopleBadge.title = "";
|
||||||
// Self dot
|
// Self dot
|
||||||
const selfDot = document.createElement("span");
|
const selfDot = document.createElement("span");
|
||||||
selfDot.className = "dot";
|
selfDot.className = "dot";
|
||||||
|
|
@ -3464,30 +3450,28 @@
|
||||||
dotCount++;
|
dotCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
peopleCount.textContent = isMultiplayer ? totalCount : "—";
|
peopleCount.textContent = connState === "connected" ? totalCount : "\u2014";
|
||||||
// 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 = `<span class="conn-dot${pulse}" style="background:${color}"></span>${label}`;
|
|
||||||
peopleConnStatus.classList.add("visible");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPeoplePanel() {
|
function renderPeoplePanel() {
|
||||||
peopleList.innerHTML = "";
|
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");
|
const selfRow = document.createElement("div");
|
||||||
selfRow.className = "people-row";
|
selfRow.className = "people-row";
|
||||||
selfRow.innerHTML = `<span class="dot" style="background:${isMultiplayer ? escapeHtml(localColor) : '#64748b'}"></span>
|
selfRow.innerHTML = `<span class="dot" style="background:${escapeHtml(localColor)}"></span>
|
||||||
<span class="name">${escapeHtml(storedUsername)} <span class="you-tag">(you)</span></span>
|
<span class="name">${escapeHtml(storedUsername)} <span class="you-tag">(you)</span></span>
|
||||||
<span class="mode-toggle">
|
<span class="mode-toggle" title="Toggle cursor sharing with other users">
|
||||||
<button class="mode-solo ${isMultiplayer ? '' : 'active'}">Offline</button>
|
<button class="mode-solo ${isMultiplayer ? '' : 'active'}">Solo</button>
|
||||||
<button class="mode-multi ${isMultiplayer ? 'active' : ''}">Online</button>
|
<button class="mode-multi ${isMultiplayer ? 'active' : ''}">Share</button>
|
||||||
</span>`;
|
</span>`;
|
||||||
selfRow.querySelector(".mode-solo").addEventListener("click", () => setMultiplayerMode(false));
|
selfRow.querySelector(".mode-solo").addEventListener("click", () => setMultiplayerMode(false));
|
||||||
selfRow.querySelector(".mode-multi").addEventListener("click", () => setMultiplayerMode(true));
|
selfRow.querySelector(".mode-multi").addEventListener("click", () => setMultiplayerMode(true));
|
||||||
|
|
@ -6628,10 +6612,18 @@
|
||||||
updateCanvasTransform();
|
updateCanvasTransform();
|
||||||
}, { passive: false });
|
}, { 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 ──
|
// ── Space key tracking for space+drag pan ──
|
||||||
let spaceHeld = false;
|
let spaceHeld = false;
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.code === "Space" && !e.target.closest("input, textarea, [contenteditable]")) {
|
if (e.code === "Space" && !isInTextInput(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
spaceHeld = true;
|
spaceHeld = true;
|
||||||
canvas.style.cursor = "grab";
|
canvas.style.cursor = "grab";
|
||||||
|
|
@ -6663,7 +6655,7 @@
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if ((e.key === "Delete" || e.key === "Backspace") &&
|
if ((e.key === "Delete" || e.key === "Backspace") &&
|
||||||
!e.target.closest("input, textarea, [contenteditable]") &&
|
!isInTextInput(e) &&
|
||||||
!bulkDeleteOverlay &&
|
!bulkDeleteOverlay &&
|
||||||
selectedShapeIds.size > 0) {
|
selectedShapeIds.size > 0) {
|
||||||
if (selectedShapeIds.size > 5) {
|
if (selectedShapeIds.size > 5) {
|
||||||
|
|
@ -6677,7 +6669,7 @@
|
||||||
// ── Undo / Redo (Ctrl+Z / Ctrl+Shift+Z) ──
|
// ── Undo / Redo (Ctrl+Z / Ctrl+Shift+Z) ──
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if ((e.key === "z" || e.key === "Z") && (e.ctrlKey || e.metaKey) &&
|
if ((e.key === "z" || e.key === "Z") && (e.ctrlKey || e.metaKey) &&
|
||||||
!e.target.closest("input, textarea, [contenteditable]")) {
|
!isInTextInput(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
sync.redo();
|
sync.redo();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue