fix(rmaps): join form overlay + authenticated user auto-join

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 18:04:24 -07:00
parent 676e29902e
commit 358965cb61
2 changed files with 456 additions and 207 deletions

View File

@ -287,16 +287,108 @@ class FolkMapViewer extends HTMLElement {
if (this.userName) return true; if (this.userName) return true;
// Use EncryptID username if authenticated // Use EncryptID username if authenticated
const identityName = getUsername(); const identityName = getUsername();
const name = identityName || prompt("Your display name for this room:"); if (identityName) {
if (!name?.trim()) return false; this.userName = identityName;
this.userName = name.trim(); localStorage.setItem("rmaps_user", JSON.stringify({
localStorage.setItem("rmaps_user", JSON.stringify({ id: this.participantId,
id: this.participantId, name: this.userName,
name: this.userName, emoji: this.userEmoji,
emoji: this.userEmoji, color: this.userColor,
color: this.userColor, }));
})); return true;
return true; }
return false;
}
private pendingRoomSlug = "";
private showJoinForm(slug: string) {
this.pendingRoomSlug = slug;
const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
const savedName = saved?.name || "";
const savedEmoji = saved?.emoji || this.userEmoji;
const overlay = document.createElement("div");
overlay.id = "join-form-overlay";
overlay.style.cssText = `
position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);
display:flex;align-items:center;justify-content:center;
backdrop-filter:blur(4px);
`;
const card = document.createElement("div");
card.style.cssText = `
background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
border-radius:16px;padding:28px;width:340px;max-width:90vw;
box-shadow:0 16px 48px rgba(0,0,0,0.4);text-align:center;
font-family:system-ui,-apple-system,sans-serif;
`;
card.innerHTML = `
<div style="font-size:48px;margin-bottom:4px;" id="join-avatar-preview">${savedEmoji}</div>
<div style="font-size:18px;font-weight:700;color:var(--rs-text-primary);margin-bottom:4px;">Join ${this.esc(slug)}</div>
<div style="font-size:13px;color:var(--rs-text-muted);margin-bottom:16px;">Choose your avatar and display name</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-bottom:16px;">
${EMOJIS.map(e => `<button class="join-emoji-opt" data-e="${e}" style="width:36px;height:36px;border-radius:8px;border:2px solid ${e === savedEmoji ? "#4f46e5" : "transparent"};background:${e === savedEmoji ? "#4f46e520" : "var(--rs-bg-surface-sunken)"};font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:border-color 0.15s;">${e}</button>`).join("")}
</div>
<input id="join-name-input" type="text" placeholder="Your display name" value="${this.esc(savedName)}" style="
width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--rs-border);
background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:15px;
outline:none;margin-bottom:12px;box-sizing:border-box;
">
${savedName ? `<button id="join-quick-btn" style="
width:100%;padding:10px;border-radius:8px;border:none;
background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:14px;
margin-bottom:8px;
">Join as ${this.esc(savedEmoji)} ${this.esc(savedName)}</button>` : ""}
<button id="join-submit-btn" style="
width:100%;padding:10px;border-radius:8px;border:1px solid var(--rs-border);
background:var(--rs-bg-surface);color:var(--rs-text-primary);font-weight:600;cursor:pointer;font-size:14px;
">${savedName ? "Join with new name" : "Join Room"}</button>
<button id="join-cancel-btn" style="
margin-top:8px;background:none;border:none;color:var(--rs-text-muted);
cursor:pointer;font-size:12px;padding:4px;
">Cancel</button>
`;
overlay.appendChild(card);
this.shadow.appendChild(overlay);
let selectedEmoji = savedEmoji;
const preview = card.querySelector("#join-avatar-preview") as HTMLElement;
const nameInput = card.querySelector("#join-name-input") as HTMLInputElement;
card.querySelectorAll(".join-emoji-opt").forEach(btn => {
btn.addEventListener("click", () => {
selectedEmoji = (btn as HTMLElement).dataset.e!;
preview.textContent = selectedEmoji;
card.querySelectorAll(".join-emoji-opt").forEach(b => {
const isSelected = (b as HTMLElement).dataset.e === selectedEmoji;
(b as HTMLElement).style.borderColor = isSelected ? "#4f46e5" : "transparent";
(b as HTMLElement).style.background = isSelected ? "#4f46e520" : "var(--rs-bg-surface-sunken)";
});
});
});
const doJoin = (name: string, emoji: string) => {
if (!name.trim()) { nameInput.style.borderColor = "#ef4444"; return; }
this.userName = name.trim();
this.userEmoji = emoji;
localStorage.setItem("rmaps_user", JSON.stringify({
id: this.participantId, name: this.userName,
emoji: this.userEmoji, color: this.userColor,
}));
overlay.remove();
this.joinRoom(this.pendingRoomSlug);
};
card.querySelector("#join-quick-btn")?.addEventListener("click", () => doJoin(savedName, savedEmoji));
card.querySelector("#join-submit-btn")?.addEventListener("click", () => doJoin(nameInput.value, selectedEmoji));
nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") doJoin(nameInput.value, selectedEmoji); });
card.querySelector("#join-cancel-btn")?.addEventListener("click", () => overlay.remove());
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
nameInput.focus();
} }
// ─── Demo mode ─────────────────────────────────────────────── // ─── Demo mode ───────────────────────────────────────────────
@ -1071,7 +1163,10 @@ class FolkMapViewer extends HTMLElement {
// ─── Room mode: join / leave / create ──────────────────────── // ─── Room mode: join / leave / create ────────────────────────
private joinRoom(slug: string) { private joinRoom(slug: string) {
if (!this.ensureUserProfile()) return; if (!this.ensureUserProfile()) {
this.showJoinForm(slug);
return;
}
this.room = slug; this.room = slug;
this.view = "map"; this.view = "map";
this.render(); this.render();
@ -1110,10 +1205,57 @@ class FolkMapViewer extends HTMLElement {
private createRoom() { private createRoom() {
if (!requireAuth("create map room")) return; if (!requireAuth("create map room")) return;
const name = prompt("Room name (slug):"); this.showCreateRoomForm();
if (!name?.trim()) return; }
const slug = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-");
this.joinRoom(slug); private showCreateRoomForm() {
const overlay = document.createElement("div");
overlay.id = "create-room-overlay";
overlay.style.cssText = `
position:fixed;inset:0;z-index:50;background:rgba(0,0,0,0.6);
display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);
`;
const card = document.createElement("div");
card.style.cssText = `
background:var(--rs-bg-surface);border:1px solid var(--rs-border-strong);
border-radius:16px;padding:28px;width:340px;max-width:90vw;
box-shadow:0 16px 48px rgba(0,0,0,0.4);text-align:center;
font-family:system-ui,-apple-system,sans-serif;
`;
card.innerHTML = `
<div style="font-size:32px;margin-bottom:8px;">&#127760;</div>
<div style="font-size:18px;font-weight:700;color:var(--rs-text-primary);margin-bottom:4px;">Create Room</div>
<div style="font-size:13px;color:var(--rs-text-muted);margin-bottom:16px;">Enter a name for your new map room</div>
<input id="room-name-input" type="text" placeholder="Room name (e.g. berlin-meetup)" style="
width:100%;padding:10px 14px;border-radius:8px;border:1px solid var(--rs-border);
background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:15px;
outline:none;margin-bottom:12px;box-sizing:border-box;
">
<button id="room-create-btn" style="
width:100%;padding:10px;border-radius:8px;border:none;
background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:14px;
">Create & Join</button>
<button id="room-cancel-btn" style="
margin-top:8px;background:none;border:none;color:var(--rs-text-muted);
cursor:pointer;font-size:12px;padding:4px;
">Cancel</button>
`;
overlay.appendChild(card);
this.shadow.appendChild(overlay);
const input = card.querySelector("#room-name-input") as HTMLInputElement;
const doCreate = () => {
const name = input.value.trim();
if (!name) { input.style.borderColor = "#ef4444"; return; }
const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
overlay.remove();
this.joinRoom(slug);
};
card.querySelector("#room-create-btn")?.addEventListener("click", doCreate);
input.addEventListener("keydown", (e) => { if (e.key === "Enter") doCreate(); });
card.querySelector("#room-cancel-btn")?.addEventListener("click", () => overlay.remove());
overlay.addEventListener("click", (e) => { if (e.target === overlay) overlay.remove(); });
input.focus();
} }
private leaveRoom() { private leaveRoom() {
@ -1258,6 +1400,7 @@ class FolkMapViewer extends HTMLElement {
}); });
saveRoomVisit(this.room, this.room); saveRoomVisit(this.room, this.room);
try { localStorage.setItem("rmaps_last_room", this.room); } catch {}
// Listen for SW-forwarded location request pushes // Listen for SW-forwarded location request pushes
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
@ -1379,6 +1522,21 @@ class FolkMapViewer extends HTMLElement {
emojiSpan.className = "marker-emoji"; emojiSpan.className = "marker-emoji";
emojiSpan.textContent = p.emoji; emojiSpan.textContent = p.emoji;
el.appendChild(emojiSpan); el.appendChild(emojiSpan);
// Status dot overlay (bottom-right corner)
const statusDot = document.createElement("div");
statusDot.className = "marker-status-dot";
const dotColor = isStale ? "#6b7280" : (
p.status === "online" ? "#22c55e" : p.status === "away" ? "#f59e0b" :
p.status === "ghost" ? "#64748b" : "#ef4444"
);
statusDot.style.cssText = `
position:absolute;bottom:-1px;right:-1px;
width:10px;height:10px;border-radius:50%;
background:${dotColor};border:2px solid ${markerBg};
box-shadow:0 0 6px ${dotColor};
`;
el.appendChild(statusDot);
} }
// Staleness tooltip // Staleness tooltip
@ -1477,6 +1635,13 @@ class FolkMapViewer extends HTMLElement {
this.indoorView.updateParticipants(state.participants); this.indoorView.updateParticipants(state.participants);
} }
// Update participant counts (header + mobile)
const pCount = String(Object.keys(state.participants).length);
const headerCount = this.shadow.getElementById("header-participant-count");
if (headerCount) headerCount.textContent = pCount;
const mobileCount = this.shadow.getElementById("mobile-friends-count");
if (mobileCount) mobileCount.textContent = pCount;
// Update participant list sidebar // Update participant list sidebar
this.updateParticipantList(state); this.updateParticipantList(state);
} }
@ -1508,11 +1673,12 @@ class FolkMapViewer extends HTMLElement {
const ageSec = Math.floor(ageMs / 1000); const ageSec = Math.floor(ageMs / 1000);
ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale"; ageLabel = ageSec < 60 ? `${ageSec}s ago` : ageSec < 3600 ? `${Math.floor(ageSec / 60)}m ago` : "stale";
} }
const glowColor = isStale ? "#6b7280" : statusColor;
return ` return `
<div class="participant-entry" style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--rs-border);${isStale ? "opacity:0.6;" : ""}"> <div class="participant-entry" style="display:flex;align-items:center;gap:8px;padding:8px 4px;border-bottom:1px solid var(--rs-border);${isStale ? "opacity:0.6;" : ""}">
<div style="position:relative;"> <div style="position:relative;flex-shrink:0;">
<span style="font-size:18px">${this.esc(p.emoji)}</span> <span style="font-size:20px;display:block;width:28px;text-align:center;">${this.esc(p.emoji)}</span>
<span style="position:absolute;bottom:-2px;right:-2px;width:8px;height:8px;border-radius:50%;background:${isStale ? "#6b7280" : statusColor};border:2px solid var(--rs-bg-surface);"></span> <span style="position:absolute;bottom:-1px;right:-1px;width:10px;height:10px;border-radius:50%;background:${glowColor};border:2px solid var(--rs-bg-surface);box-shadow:0 0 6px ${glowColor};"></span>
</div> </div>
<div style="flex:1;min-width:0;"> <div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:${isStale ? "#6b7280" : p.color};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${this.esc(p.name)}</div> <div style="font-size:13px;font-weight:600;color:${isStale ? "#6b7280" : p.color};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${this.esc(p.name)}</div>
@ -1521,8 +1687,8 @@ class FolkMapViewer extends HTMLElement {
${distLabel ? ` \u2022 ${distLabel}` : ""} ${distLabel ? ` \u2022 ${distLabel}` : ""}
</div> </div>
</div> </div>
${p.id !== this.participantId && p.location ? `<button class="nav-to-btn" data-nav-participant="${p.id}" title="Navigate" style="background:none;border:1px solid var(--rs-border);border-radius:4px;color:var(--rs-text-muted);cursor:pointer;padding:2px 6px;font-size:11px;">\u{1F9ED}</button>` : ""} ${p.id !== this.participantId && p.location ? `<button class="nav-to-btn" data-nav-participant="${p.id}" title="Navigate" style="background:none;border:1px solid var(--rs-border);border-radius:6px;color:var(--rs-text-muted);cursor:pointer;padding:3px 8px;font-size:12px;">&#129517;</button>` : ""}
${p.id !== this.participantId ? `<button class="ping-btn-inline" data-ping="${p.id}" title="Ping" style="background:none;border:1px solid var(--rs-border);border-radius:4px;color:var(--rs-text-muted);cursor:pointer;padding:2px 6px;font-size:11px;">\u{1F514}</button>` : ""} ${p.id !== this.participantId ? `<button class="ping-btn-inline" data-ping="${p.id}" title="Ping" style="background:none;border:1px solid var(--rs-border);border-radius:6px;color:var(--rs-text-muted);cursor:pointer;padding:3px 8px;font-size:12px;">&#128276;</button>` : ""}
</div>`; </div>`;
}).join(""); }).join("");
} }
@ -1562,29 +1728,33 @@ class FolkMapViewer extends HTMLElement {
const list = this.shadow.getElementById("participant-list"); const list = this.shadow.getElementById("participant-list");
const mobileList = this.shadow.getElementById("participant-list-mobile"); const mobileList = this.shadow.getElementById("participant-list-mobile");
const count = Object.keys(state.participants).length;
const html = this.buildParticipantHTML(state); const html = this.buildParticipantHTML(state);
const headerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<span style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);text-transform:uppercase;letter-spacing:0.06em;">Friends (${count})</span>
<button id="sidebar-ping-all-btn" style="padding:3px 8px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:10px;white-space:nowrap;">&#128226; Ping All</button>
</div>
`;
const footerHTML = ` const footerHTML = `
<div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap;"> <div style="display:flex;gap:6px;margin-top:10px;flex-wrap:wrap;">
<button id="sidebar-meeting-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">\u{1F4CD} Meeting Point</button> <button id="sidebar-meeting-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">&#128205; Meeting Point</button>
<button id="sidebar-import-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">\u{1F4E5} Import Places</button> <button id="sidebar-import-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">&#128229; Import Places</button>
<button id="sidebar-ping-all-btn" style="flex:1;padding:6px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:11px;white-space:nowrap;">\u{1F4E2} Ping All</button>
</div> </div>
`; `;
// Desktop sidebar // Desktop sidebar
if (list) { if (list) {
list.innerHTML = html + footerHTML; list.innerHTML = headerHTML + html + footerHTML;
this.attachParticipantListeners(list); this.attachParticipantListeners(list);
} }
// Mobile bottom sheet // Mobile bottom sheet
if (mobileList) { if (mobileList) {
mobileList.innerHTML = html; mobileList.innerHTML = html + footerHTML;
this.attachParticipantListeners(mobileList); this.attachParticipantListeners(mobileList);
// Update sheet header count const sheetCount = this.shadow.getElementById("sheet-participant-count");
const header = this.shadow.querySelector(".sheet-header span:first-child"); if (sheetCount) sheetCount.textContent = String(count);
const count = Object.keys(state.participants).length;
if (header) header.textContent = `Participants (${count})`;
} }
} }
@ -1622,6 +1792,16 @@ class FolkMapViewer extends HTMLElement {
el.title = `${p.name} - ${p.status} (${ageLabel})`; el.title = `${p.name} - ${p.status} (${ageLabel})`;
const arrow = el.querySelector(".heading-arrow") as HTMLElement | null; const arrow = el.querySelector(".heading-arrow") as HTMLElement | null;
if (arrow) arrow.style.borderBottomColor = isStale ? "#6b7280" : p.color; if (arrow) arrow.style.borderBottomColor = isStale ? "#6b7280" : p.color;
// Update status dot
const statusDot = el.querySelector(".marker-status-dot") as HTMLElement | null;
if (statusDot) {
const dotColor = isStale ? "#6b7280" : (
p.status === "online" ? "#22c55e" : p.status === "away" ? "#f59e0b" :
p.status === "ghost" ? "#64748b" : "#ef4444"
);
statusDot.style.background = dotColor;
statusDot.style.boxShadow = `0 0 6px ${dotColor}`;
}
} }
} }
@ -1786,19 +1966,35 @@ class FolkMapViewer extends HTMLElement {
private updateShareButton() { private updateShareButton() {
const btn = this.shadow.getElementById("share-location"); const btn = this.shadow.getElementById("share-location");
if (!btn) return; if (btn) {
if (this.privacySettings.precision === "hidden") { if (this.privacySettings.precision === "hidden") {
btn.textContent = "\u{1F47B} Hidden"; btn.textContent = "\u{1F47B} Hidden";
btn.classList.remove("sharing"); btn.classList.remove("sharing");
btn.classList.add("ghost"); btn.classList.add("ghost");
} else if (this.sharingLocation) { } else if (this.sharingLocation) {
btn.textContent = "\u{1F4CD} Stop Sharing"; btn.textContent = "\u{1F4CD} Stop Sharing";
btn.classList.add("sharing"); btn.classList.add("sharing");
btn.classList.remove("ghost"); btn.classList.remove("ghost");
} else { } else {
btn.textContent = "\u{1F4CD} Share Location"; btn.textContent = "\u{1F4CD} Share Location";
btn.classList.remove("sharing"); btn.classList.remove("sharing");
btn.classList.remove("ghost"); btn.classList.remove("ghost");
}
}
// Floating share button
const floatBtn = this.shadow.getElementById("map-share-float");
if (floatBtn) {
if (this.privacySettings.precision === "hidden") {
floatBtn.innerHTML = "&#128123; Hidden";
floatBtn.className = "map-share-float ghost";
} else if (this.sharingLocation) {
floatBtn.innerHTML = "&#128205; Sharing";
floatBtn.className = "map-share-float active";
} else {
floatBtn.innerHTML = "&#128205; Share Location";
floatBtn.className = "map-share-float";
}
} }
// Update permission indicator // Update permission indicator
const permIndicator = this.shadow.getElementById("geo-perm-indicator"); const permIndicator = this.shadow.getElementById("geo-perm-indicator");
@ -1807,6 +2003,11 @@ class FolkMapViewer extends HTMLElement {
permIndicator.style.background = colors[this.geoPermissionState] || "#64748b"; permIndicator.style.background = colors[this.geoPermissionState] || "#64748b";
permIndicator.title = `Geolocation: ${this.geoPermissionState || "unknown"}`; permIndicator.title = `Geolocation: ${this.geoPermissionState || "unknown"}`;
} }
// Update header share toggle
const headerToggle = this.shadow.getElementById("header-share-toggle");
if (headerToggle) {
headerToggle.className = `map-header__share-toggle ${this.sharingLocation ? "active" : ""}`;
}
// Also update mobile FAB // Also update mobile FAB
this.updateMobileFab(); this.updateMobileFab();
} }
@ -2369,11 +2570,49 @@ class FolkMapViewer extends HTMLElement {
:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); } :host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
* { box-sizing: border-box; } * { box-sizing: border-box; }
/* Lobby nav */
.rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; } .rapp-nav { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; min-height: 36px; }
.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; } .rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid var(--rs-border); background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; }
.rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); } .rapp-nav__back:hover { color: var(--rs-text-primary); border-color: var(--rs-border-strong); }
.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Map room header — dark bar */
.map-header {
display:flex;align-items:center;gap:8px;padding:8px 14px;
background:rgba(15,23,42,0.95);backdrop-filter:blur(8px);
border-radius:10px;margin-bottom:12px;min-height:44px;
border:1px solid rgba(255,255,255,0.08);
}
.map-header__left { display:flex;align-items:center;gap:6px;flex:1;min-width:0; }
.map-header__back {
background:none;border:none;color:#94a3b8;cursor:pointer;
font-size:16px;padding:4px 6px;border-radius:4px;
}
.map-header__back:hover { color:#e2e8f0; }
.map-header__logo { font-size:13px;font-weight:700;color:#e2e8f0;white-space:nowrap; }
.map-header__slug { font-size:12px;color:#64748b;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.map-header__participants {
display:flex;align-items:center;gap:4px;
padding:4px 12px;border-radius:20px;
background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);
color:#e2e8f0;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;
}
.map-header__participants:hover { background:rgba(255,255,255,0.14); }
.map-header__right { display:flex;align-items:center;gap:4px; }
.map-header__icon-btn {
background:none;border:none;color:#94a3b8;cursor:pointer;
font-size:15px;padding:4px 6px;border-radius:4px;
}
.map-header__icon-btn:hover { color:#e2e8f0; }
.map-header__share-toggle {
width:32px;height:32px;border-radius:50%;
background:transparent;border:1px solid rgba(255,255,255,0.15);
color:#94a3b8;cursor:pointer;font-size:14px;
display:flex;align-items:center;justify-content:center;transition:all 0.2s;
}
.map-header__share-toggle.active { background:#10b981;border-color:#10b981;color:#fff; }
.map-header__share-toggle:hover { border-color:#10b981;color:#10b981; }
.status-dot { .status-dot {
width: 8px; height: 8px; border-radius: 50%; display: inline-block; width: 8px; height: 8px; border-radius: 50%; display: inline-block;
} }
@ -2448,8 +2687,21 @@ class FolkMapViewer extends HTMLElement {
} }
.map-locate-fab:hover { border-color: #4285f4; color: #4285f4; } .map-locate-fab:hover { border-color: #4285f4; color: #4285f4; }
/* Mobile FAB menu — hidden on desktop */ /* Floating share-location button on map */
.mobile-fab-container { display: none; } .map-share-float {
position:absolute;bottom:20px;right:16px;z-index:6;
padding:10px 18px;border-radius:24px;
background:#fff;border:1px solid #e2e8f0;
color:#1e293b;font-weight:600;font-size:13px;cursor:pointer;
box-shadow:0 2px 10px rgba(0,0,0,0.15);transition:all 0.2s;
font-family:system-ui,-apple-system,sans-serif;
}
.map-share-float.active { background:#10b981;border-color:#10b981;color:#fff; }
.map-share-float.ghost { background:#8b5cf6;border-color:#8b5cf6;color:#fff; }
.map-share-float:hover { box-shadow:0 4px 16px rgba(0,0,0,0.2); }
/* Mobile floating buttons — hidden on desktop */
.mobile-float-btns { display: none; }
.mobile-bottom-sheet { display: none; } .mobile-bottom-sheet { display: none; }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -2458,52 +2710,20 @@ class FolkMapViewer extends HTMLElement {
.map-sidebar { display: none; } .map-sidebar { display: none; }
.controls { display: none; } .controls { display: none; }
#privacy-panel { display: none !important; } #privacy-panel { display: none !important; }
.rapp-nav { position: absolute; top: 0; left: 0; right: 0; z-index: 7; background: var(--rs-bg-surface); border-bottom: 1px solid var(--rs-border); margin: 0; padding: 6px 12px; min-height: 48px; } .map-header { position:absolute;top:0;left:0;right:0;z-index:7;margin:0;border-radius:0;border-bottom:1px solid rgba(255,255,255,0.08); }
.map-locate-fab { bottom: 100px; } .map-locate-fab { bottom: 100px; }
/* Mobile FAB menu */ /* Mobile floating pill buttons */
.mobile-fab-container { .mobile-float-btns { display:flex;position:fixed;bottom:16px;left:0;right:0;z-index:8;padding:0 16px;justify-content:space-between;pointer-events:none; }
display: block; position: fixed; bottom: 24px; right: 16px; z-index: 8; .mobile-pill-btn {
padding:10px 16px;border-radius:24px;border:none;
background:rgba(15,23,42,0.92);color:#e2e8f0;font-weight:600;
font-size:13px;cursor:pointer;pointer-events:auto;
box-shadow:0 4px 16px rgba(0,0,0,0.3);backdrop-filter:blur(4px);
font-family:system-ui,-apple-system,sans-serif;
} }
.fab-main { .mobile-pill-btn:active { opacity:0.8; }
width: 52px; height: 52px; border-radius: 50%; .map-share-float { bottom:70px; }
background: #4f46e5; border: none; color: #fff; cursor: pointer;
font-size: 22px; display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 16px rgba(79,70,229,0.5); transition: transform 0.2s;
}
.fab-main.open { transform: rotate(45deg); }
.fab-mini-list {
position: absolute; bottom: 62px; right: 2px;
display: flex; flex-direction: column-reverse; gap: 10px;
opacity: 0; pointer-events: none; transition: opacity 0.2s;
}
.fab-mini-list.open { opacity: 1; pointer-events: auto; }
.fab-mini {
display: flex; align-items: center; gap: 8px; flex-direction: row-reverse;
}
.fab-mini-btn {
width: 40px; height: 40px; border-radius: 50%;
border: 1px solid var(--rs-border); background: var(--rs-bg-surface);
color: var(--rs-text-primary); cursor: pointer; font-size: 16px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
transform: scale(0); transition: transform 0.15s;
}
.fab-mini-list.open .fab-mini-btn { transform: scale(1); }
.fab-mini-list.open .fab-mini:nth-child(1) .fab-mini-btn { transition-delay: 0s; }
.fab-mini-list.open .fab-mini:nth-child(2) .fab-mini-btn { transition-delay: 0.04s; }
.fab-mini-list.open .fab-mini:nth-child(3) .fab-mini-btn { transition-delay: 0.08s; }
.fab-mini-list.open .fab-mini:nth-child(4) .fab-mini-btn { transition-delay: 0.12s; }
.fab-mini-list.open .fab-mini:nth-child(5) .fab-mini-btn { transition-delay: 0.16s; }
.fab-mini-list.open .fab-mini:nth-child(6) .fab-mini-btn { transition-delay: 0.20s; }
.fab-mini-list.open .fab-mini:nth-child(7) .fab-mini-btn { transition-delay: 0.24s; }
.fab-mini-label {
font-size: 11px; background: var(--rs-bg-surface); color: var(--rs-text-secondary);
padding: 4px 8px; border-radius: 6px; white-space: nowrap;
box-shadow: 0 1px 4px rgba(0,0,0,0.15); border: 1px solid var(--rs-border);
}
.fab-mini-btn.sharing { border-color: #22c55e; color: #22c55e; }
.fab-mini-btn.ghost { border-color: #8b5cf6; color: #8b5cf6; }
/* Mobile bottom sheet */ /* Mobile bottom sheet */
.mobile-bottom-sheet { .mobile-bottom-sheet {
@ -2611,6 +2831,37 @@ class FolkMapViewer extends HTMLElement {
startTour() { this._tour.start(); } startTour() { this._tour.start(); }
private renderLobby(): string { private renderLobby(): string {
const saved = JSON.parse(localStorage.getItem("rmaps_user") || "null");
const lastRoom = localStorage.getItem("rmaps_last_room") || "";
const profileSection = `
<div style="background:var(--rs-glass-bg);border:1px solid var(--rs-border);border-radius:12px;padding:16px;margin-bottom:16px;">
<div style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:10px;">Get Started</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<div style="position:relative;">
<span id="lobby-avatar-preview" style="font-size:32px;display:block;cursor:pointer;" title="Click to change emoji">${this.userEmoji}</span>
</div>
<div style="flex:1;">
<input id="lobby-name-input" type="text" placeholder="Your display name" value="${this.esc(saved?.name || this.userName || "")}" style="
width:100%;padding:8px 12px;border-radius:8px;border:1px solid var(--rs-border);
background:var(--rs-input-bg);color:var(--rs-text-primary);font-size:14px;outline:none;box-sizing:border-box;
">
</div>
</div>
<div id="lobby-emoji-grid" style="display:none;flex-wrap:wrap;gap:4px;margin-bottom:10px;justify-content:center;">
${EMOJIS.map(e => `<button class="lobby-emoji-pick" data-lobby-emoji="${e}" style="width:34px;height:34px;border-radius:6px;border:2px solid ${e === this.userEmoji ? "#4f46e5" : "transparent"};background:${e === this.userEmoji ? "#4f46e520" : "var(--rs-bg-surface-sunken)"};font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;">${e}</button>`).join("")}
</div>
${lastRoom ? `
<button id="lobby-rejoin-btn" style="
width:100%;padding:10px;border-radius:8px;border:none;
background:#4f46e5;color:#fff;font-weight:600;cursor:pointer;font-size:13px;
margin-bottom:6px;
">Rejoin &#127760; ${this.esc(lastRoom)}</button>
` : ""}
<div style="font-size:11px;color:var(--rs-text-muted);text-align:center;">Profile is saved locally for quick rejoins</div>
</div>
`;
const history = loadRoomHistory(); const history = loadRoomHistory();
const historyCards = history.length > 0 ? ` const historyCards = history.length > 0 ? `
<div class="section-label">Recent Rooms</div> <div class="section-label">Recent Rooms</div>
@ -2640,6 +2891,8 @@ class FolkMapViewer extends HTMLElement {
<button class="rapp-nav__btn" id="btn-tour" style="background:transparent;border:1px solid var(--rs-border,#334155);color:var(--rs-text-secondary,#94a3b8);font-weight:500">Tour</button> <button class="rapp-nav__btn" id="btn-tour" style="background:transparent;border:1px solid var(--rs-border,#334155);color:var(--rs-text-secondary,#94a3b8);font-weight:500">Tour</button>
</div> </div>
${profileSection}
${this.rooms.length > 0 ? ` ${this.rooms.length > 0 ? `
<div class="section-label">Active Rooms</div> <div class="section-label">Active Rooms</div>
${this.rooms.map((r) => ` ${this.rooms.map((r) => `
@ -2661,12 +2914,24 @@ class FolkMapViewer extends HTMLElement {
private renderMap(): string { private renderMap(): string {
return ` return `
<div class="rapp-nav"> <div class="map-header">
${this._history.canGoBack ? '<button class="rapp-nav__back" data-back="lobby">&#8592; Rooms</button>' : ''} <div class="map-header__left">
<span class="rapp-nav__title">\u{1F5FA} ${this.esc(this.room)}</span> ${this._history.canGoBack ? '<button class="map-header__back" data-back="lobby">&#8592;</button>' : ''}
<span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}"></span> <span class="map-header__logo">&#127760; rMaps</span>
<span id="geo-perm-indicator" style="width:6px;height:6px;border-radius:50%;background:#64748b;" title="Geolocation: unknown"></span> <span class="map-header__slug">/${this.esc(this.room)}</span>
<button id="bell-toggle" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:16px;padding:4px;" title="Notifications">\u{1F514}</button> <span class="status-dot ${this.syncStatus === "connected" ? "status-connected" : "status-disconnected"}" style="margin-left:6px;"></span>
</div>
<button class="map-header__participants" id="header-participants-toggle" title="Show participants">
&#128101; <span id="header-participant-count">0</span>
</button>
<div class="map-header__right">
<span id="geo-perm-indicator" style="width:6px;height:6px;border-radius:50%;background:#64748b;" title="Geolocation: unknown"></span>
<button class="map-header__icon-btn" id="bell-toggle" title="Notifications">&#128276;</button>
<button class="map-header__icon-btn" id="share-room-btn" title="Share rMap">&#128228;</button>
<button class="map-header__share-toggle ${this.sharingLocation ? "active" : ""}" id="header-share-toggle" title="${this.sharingLocation ? "Sharing location" : "Share location"}">
${this.sharingLocation ? "&#128205;" : "&#128205;"}
</button>
</div>
</div> </div>
<div class="map-layout"> <div class="map-layout">
@ -2705,54 +2970,30 @@ class FolkMapViewer extends HTMLElement {
<button class="map-locate-fab" id="locate-me-fab" title="Center on my location">\u{1F3AF}</button> <button class="map-locate-fab" id="locate-me-fab" title="Center on my location">\u{1F3AF}</button>
<button class="map-locate-fab" id="fit-all-fab" title="Show all participants" style="bottom:80px;">\u{1F465}</button> <button class="map-locate-fab" id="fit-all-fab" title="Show all participants" style="bottom:80px;">\u{1F465}</button>
<!-- Mobile FAB menu --> <!-- Mobile floating buttons (rmaps-online style) -->
<div class="mobile-fab-container" id="mobile-fab-container"> <div class="mobile-float-btns">
<div class="fab-mini-list" id="fab-mini-list"> <button class="mobile-pill-btn mobile-pill-left" id="mobile-friends-btn">&#128101; See Friends (<span id="mobile-friends-count">0</span>)</button>
<div class="fab-mini"> <button class="mobile-pill-btn mobile-pill-right" id="mobile-qr-btn">&#9641; QR</button>
<button class="fab-mini-btn ${this.sharingLocation ? "sharing" : ""} ${this.privacySettings.ghostMode ? "ghost" : ""}" id="fab-share-loc" title="Share Location">\u{1F4CD}</button>
<span class="fab-mini-label">${this.privacySettings.ghostMode ? "Ghost" : this.sharingLocation ? "Stop" : "Share"}</span>
</div>
<div class="fab-mini">
<button class="fab-mini-btn" id="fab-privacy" title="Privacy">\u{1F6E1}</button>
<span class="fab-mini-label">Privacy</span>
</div>
<div class="fab-mini">
<button class="fab-mini-btn" id="fab-drop-pin" title="Drop Pin">\u{1F4CC}</button>
<span class="fab-mini-label">Drop Pin</span>
</div>
<div class="fab-mini">
<button class="fab-mini-btn" id="fab-share-map" title="Share rMap">\u{1F4E4}</button>
<span class="fab-mini-label">Share rMap</span>
</div>
<div class="fab-mini">
<button class="fab-mini-btn" id="fab-emoji" title="Emoji">${this.userEmoji}</button>
<span class="fab-mini-label">Emoji</span>
</div>
<div class="fab-mini">
<button class="fab-mini-btn" id="fab-chat" title="Chat" style="position:relative;">\u{1F4AC}<span id="fab-chat-badge" style="display:${this.unreadCount > 0 ? "flex" : "none"};position:absolute;top:-4px;right:-4px;min-width:14px;height:14px;border-radius:7px;background:#ef4444;color:#fff;font-size:8px;align-items:center;justify-content:center;padding:0 3px;">${this.unreadCount || ""}</span></button>
<span class="fab-mini-label">Chat</span>
</div>
<div class="fab-mini">
<button class="fab-mini-btn" id="fab-indoor" title="Indoor">${this.mapMode === "indoor" ? "\u{1F30D}" : "\u{1F3E2}"}</button>
<span class="fab-mini-label">${this.mapMode === "indoor" ? "Outdoor" : "Indoor"}</span>
</div>
</div>
<button class="fab-main" id="fab-main" title="Menu">\u2699</button>
</div> </div>
<!-- Floating share-location button on map -->
<button class="map-share-float ${this.sharingLocation ? "active" : ""} ${this.privacySettings.precision === "hidden" ? "ghost" : ""}" id="map-share-float">
${this.privacySettings.precision === "hidden" ? "&#128123; Hidden" : this.sharingLocation ? "&#128205; Sharing" : "&#128205; Share Location"}
</button>
<!-- Mobile bottom sheet (participants) --> <!-- Mobile bottom sheet (participants) -->
<div class="mobile-bottom-sheet" id="mobile-bottom-sheet"> <div class="mobile-bottom-sheet" id="mobile-bottom-sheet">
<div class="sheet-handle" id="sheet-handle"> <div class="sheet-handle" id="sheet-handle">
<div class="sheet-handle-bar"></div> <div class="sheet-handle-bar"></div>
</div> </div>
<div class="sheet-header"> <div class="sheet-header">
<span>Participants</span> <span>Friends (<span id="sheet-participant-count">0</span>)</span>
<span style="font-size:10px;font-weight:400;color:var(--rs-text-muted);text-transform:none;letter-spacing:0;">tap to expand</span> <button id="sheet-close-btn" style="background:none;border:none;color:var(--rs-text-muted);cursor:pointer;font-size:14px;padding:2px 6px;">&#10005;</button>
</div> </div>
<div class="sheet-content" id="participant-list-mobile"></div> <div class="sheet-content" id="participant-list-mobile"></div>
</div> </div>
<!-- Mobile privacy popup (hidden by default) --> <!-- Mobile privacy popup (hidden by default, triggered from bottom sheet) -->
<div class="mobile-privacy-popup" id="mobile-privacy-popup" style="display:none;"></div> <div class="mobile-privacy-popup" id="mobile-privacy-popup" style="display:none;"></div>
`; `;
} }
@ -2761,6 +3002,60 @@ class FolkMapViewer extends HTMLElement {
this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour()); this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
this.shadow.getElementById("create-room")?.addEventListener("click", () => this.createRoom()); this.shadow.getElementById("create-room")?.addEventListener("click", () => this.createRoom());
// Lobby profile section
const lobbyNameInput = this.shadow.getElementById("lobby-name-input") as HTMLInputElement;
if (lobbyNameInput) {
lobbyNameInput.addEventListener("change", () => {
const name = lobbyNameInput.value.trim();
if (name) {
this.userName = name;
localStorage.setItem("rmaps_user", JSON.stringify({
id: this.participantId, name: this.userName,
emoji: this.userEmoji, color: this.userColor,
}));
}
});
}
const lobbyAvatar = this.shadow.getElementById("lobby-avatar-preview");
const lobbyEmojiGrid = this.shadow.getElementById("lobby-emoji-grid");
if (lobbyAvatar && lobbyEmojiGrid) {
lobbyAvatar.addEventListener("click", () => {
lobbyEmojiGrid.style.display = lobbyEmojiGrid.style.display === "none" ? "flex" : "none";
});
}
this.shadow.querySelectorAll("[data-lobby-emoji]").forEach(btn => {
btn.addEventListener("click", () => {
const emoji = (btn as HTMLElement).dataset.lobbyEmoji!;
this.userEmoji = emoji;
if (lobbyAvatar) lobbyAvatar.textContent = emoji;
this.shadow.querySelectorAll("[data-lobby-emoji]").forEach(b => {
const sel = (b as HTMLElement).dataset.lobbyEmoji === emoji;
(b as HTMLElement).style.borderColor = sel ? "#4f46e5" : "transparent";
(b as HTMLElement).style.background = sel ? "#4f46e520" : "var(--rs-bg-surface-sunken)";
});
localStorage.setItem("rmaps_user", JSON.stringify({
id: this.participantId, name: this.userName,
emoji: this.userEmoji, color: this.userColor,
}));
});
});
this.shadow.getElementById("lobby-rejoin-btn")?.addEventListener("click", () => {
const lastRoom = localStorage.getItem("rmaps_last_room");
if (lastRoom) {
// Save name from input if provided
if (lobbyNameInput?.value.trim()) {
this.userName = lobbyNameInput.value.trim();
localStorage.setItem("rmaps_user", JSON.stringify({
id: this.participantId, name: this.userName,
emoji: this.userEmoji, color: this.userColor,
}));
}
this._history.push("lobby");
this._history.push("map", { room: lastRoom });
this.joinRoom(lastRoom);
}
});
this.shadow.querySelectorAll("[data-room]").forEach((el) => { this.shadow.querySelectorAll("[data-room]").forEach((el) => {
el.addEventListener("click", () => { el.addEventListener("click", () => {
const room = (el as HTMLElement).dataset.room!; const room = (el as HTMLElement).dataset.room!;
@ -2780,6 +3075,22 @@ class FolkMapViewer extends HTMLElement {
this.toggleLocationSharing(); this.toggleLocationSharing();
}); });
// Header share toggle
this.shadow.getElementById("header-share-toggle")?.addEventListener("click", () => {
this.toggleLocationSharing();
});
// Header participants toggle
this.shadow.getElementById("header-participants-toggle")?.addEventListener("click", () => {
// Desktop: toggle sidebar visibility, Mobile: toggle bottom sheet
const sidebar = this.shadow.querySelector(".map-sidebar") as HTMLElement;
if (sidebar && window.innerWidth > 768) {
sidebar.style.display = sidebar.style.display === "none" ? "" : "none";
}
const sheet = this.shadow.getElementById("mobile-bottom-sheet");
if (sheet) sheet.classList.toggle("expanded");
});
this.shadow.getElementById("drop-waypoint")?.addEventListener("click", () => { this.shadow.getElementById("drop-waypoint")?.addEventListener("click", () => {
this.dropWaypoint(); this.dropWaypoint();
}); });
@ -2865,75 +3176,26 @@ class FolkMapViewer extends HTMLElement {
this.fitToParticipants(); this.fitToParticipants();
}); });
// Mobile FAB menu // Mobile floating buttons
this.shadow.getElementById("fab-main")?.addEventListener("click", () => { this.shadow.getElementById("mobile-friends-btn")?.addEventListener("click", () => {
const main = this.shadow.getElementById("fab-main"); const sheet = this.shadow.getElementById("mobile-bottom-sheet");
const list = this.shadow.getElementById("fab-mini-list"); if (sheet) sheet.classList.toggle("expanded");
if (main && list) {
const isOpen = list.classList.contains("open");
list.classList.toggle("open");
main.classList.toggle("open");
// Close mobile privacy popup when closing FAB
if (isOpen) {
const popup = this.shadow.getElementById("mobile-privacy-popup");
if (popup) popup.style.display = "none";
}
}
}); });
this.shadow.getElementById("fab-share-loc")?.addEventListener("click", () => { this.shadow.getElementById("mobile-qr-btn")?.addEventListener("click", () => {
this.toggleLocationSharing();
this.updateMobileFab();
});
this.shadow.getElementById("fab-privacy")?.addEventListener("click", () => {
const popup = this.shadow.getElementById("mobile-privacy-popup");
if (popup) {
const isVisible = popup.style.display !== "none";
popup.style.display = isVisible ? "none" : "block";
if (!isVisible) this.renderPrivacyPanel(popup);
}
});
this.shadow.getElementById("fab-drop-pin")?.addEventListener("click", () => {
this.closeMobileFab();
this.dropWaypoint();
});
this.shadow.getElementById("fab-share-map")?.addEventListener("click", () => {
this.closeMobileFab();
this.showShareModal = true; this.showShareModal = true;
this.renderShareModal(); this.renderShareModal();
}); });
this.shadow.getElementById("fab-emoji")?.addEventListener("click", () => { // Floating share-location button on map
this.showEmojiPicker = !this.showEmojiPicker; this.shadow.getElementById("map-share-float")?.addEventListener("click", () => {
this.updateEmojiButton(); this.toggleLocationSharing();
}); });
this.shadow.getElementById("fab-chat")?.addEventListener("click", () => { // Sheet close button
this.closeMobileFab(); this.shadow.getElementById("sheet-close-btn")?.addEventListener("click", () => {
// Toggle mobile bottom sheet to chat view const s = this.shadow.getElementById("mobile-bottom-sheet");
const sheet = this.shadow.getElementById("mobile-bottom-sheet"); if (s) s.classList.remove("expanded");
if (sheet) {
sheet.classList.add("expanded");
const content = this.shadow.getElementById("participant-list-mobile");
if (content && !content.querySelector("map-chat-panel")) {
this.mountChatPanel(content);
}
}
this.unreadCount = 0;
this.updateChatBadge();
});
this.shadow.getElementById("fab-indoor")?.addEventListener("click", () => {
this.closeMobileFab();
if (this.mapMode === "outdoor") {
const event = prompt("c3nav event code (e.g. 39c3):", "39c3");
if (event?.trim()) this.switchToIndoor(event.trim());
} else {
this.switchToOutdoor();
}
}); });
// Mobile bottom sheet // Mobile bottom sheet
@ -2974,21 +3236,8 @@ class FolkMapViewer extends HTMLElement {
}); });
} }
private closeMobileFab() {
const main = this.shadow.getElementById("fab-main");
const list = this.shadow.getElementById("fab-mini-list");
if (main) main.classList.remove("open");
if (list) list.classList.remove("open");
const popup = this.shadow.getElementById("mobile-privacy-popup");
if (popup) popup.style.display = "none";
}
private updateMobileFab() { private updateMobileFab() {
const btn = this.shadow.getElementById("fab-share-loc"); // No-op — legacy FAB removed; share state updated via updateShareButton()
if (!btn) return;
btn.className = `fab-mini-btn ${this.sharingLocation ? "sharing" : ""} ${this.privacySettings.ghostMode ? "ghost" : ""}`;
const label = btn.parentElement?.querySelector(".fab-mini-label");
if (label) label.textContent = this.privacySettings.ghostMode ? "Ghost" : this.sharingLocation ? "Stop" : "Share";
} }
private goBack() { private goBack() {

View File

@ -276,7 +276,7 @@ routes.get("/", (c) => {
body: `<folk-map-viewer space="${space}"></folk-map-viewer>`, body: `<folk-map-viewer space="${space}"></folk-map-viewer>`,
scripts: `<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js" as="script"> scripts: `<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js" as="script">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css" as="style"> <link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css" as="style">
<script type="module" src="/modules/rmaps/folk-map-viewer.js?v=5"></script>`, <script type="module" src="/modules/rmaps/folk-map-viewer.js?v=6"></script>`,
styles: `<link rel="stylesheet" href="/modules/rmaps/maps.css?v=3">`, styles: `<link rel="stylesheet" href="/modules/rmaps/maps.css?v=3">`,
})); }));
}); });
@ -295,7 +295,7 @@ routes.get("/:room", (c) => {
body: `<folk-map-viewer space="${space}" room="${room}"></folk-map-viewer>`, body: `<folk-map-viewer space="${space}" room="${room}"></folk-map-viewer>`,
scripts: `<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js" as="script"> scripts: `<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js" as="script">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css" as="style"> <link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css" as="style">
<script type="module" src="/modules/rmaps/folk-map-viewer.js?v=5"></script>`, <script type="module" src="/modules/rmaps/folk-map-viewer.js?v=6"></script>`,
})); }));
}); });