rspace-online/modules/rmaps/components/map-privacy-panel.ts

81 lines
3.5 KiB
TypeScript

/**
* <map-privacy-panel> — unified 5-level privacy control for rMaps.
* Dispatches 'precision-change' CustomEvent with the selected PrecisionLevel.
* "hidden" level replaces the old ghost mode toggle.
*/
import type { PrecisionLevel, PrivacySettings } from "./map-sync";
const LEVELS: { value: PrecisionLevel; label: string; icon: string; desc: string }[] = [
{ value: "exact", label: "Exact", icon: "\u{1F4CD}", desc: "Precise GPS location" },
{ value: "building", label: "~50m Building", icon: "\u{1F3E2}", desc: "Fuzzy to nearby building" },
{ value: "area", label: "~500m Area", icon: "\u{1F3D8}", desc: "Fuzzy to neighborhood" },
{ value: "approximate", label: "~5km Approx", icon: "\u{1F30D}", desc: "Fuzzy to city area" },
{ value: "hidden", label: "Hidden (Ghost)", icon: "\u{1F47B}", desc: "Stops sharing entirely" },
];
class MapPrivacyPanel extends HTMLElement {
private _settings: PrivacySettings = { precision: "exact", ghostMode: false };
static get observedAttributes() { return ["precision", "ghost"]; }
get settings(): PrivacySettings { return this._settings; }
set settings(v: PrivacySettings) {
this._settings = v;
// Sync ghost→hidden on ingest
if (v.ghostMode && v.precision !== "hidden") this._settings.precision = "hidden";
this.render();
}
attributeChangedCallback() {
this._settings.precision = (this.getAttribute("precision") as PrecisionLevel) || "exact";
this._settings.ghostMode = this.getAttribute("ghost") === "true";
if (this._settings.ghostMode && this._settings.precision !== "hidden") this._settings.precision = "hidden";
this.render();
}
connectedCallback() { this.render(); }
private render() {
const active = this._settings.ghostMode ? "hidden" : this._settings.precision;
this.innerHTML = `
<div style="font-size:12px;font-weight:600;color:var(--rs-text-secondary);margin-bottom:8px;">Privacy Level</div>
<div style="display:flex;flex-direction:column;gap:4px;">
${LEVELS.map(l => {
const isActive = l.value === active;
const borderColor = isActive ? (l.value === "hidden" ? "#8b5cf6" : "#4f46e5") : "var(--rs-border)";
const bg = isActive ? (l.value === "hidden" ? "#8b5cf620" : "#4f46e520") : "transparent";
return `
<button class="priv-level-btn" data-level="${l.value}" style="
display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:8px;
border:1px solid ${borderColor};background:${bg};
color:var(--rs-text-primary);cursor:pointer;text-align:left;font-size:12px;
transition:border-color 0.15s,background 0.15s;
">
<span style="font-size:16px;flex-shrink:0;">${l.icon}</span>
<div style="flex:1;min-width:0;">
<div style="font-weight:${isActive ? "600" : "500"};color:${isActive ? (l.value === "hidden" ? "#8b5cf6" : "#4f46e5") : "var(--rs-text-primary)"};">${l.label}</div>
<div style="font-size:10px;color:var(--rs-text-muted);margin-top:1px;">${l.desc}</div>
</div>
${isActive ? '<span style="color:#22c55e;font-size:14px;">&#10003;</span>' : ""}
</button>`;
}).join("")}
</div>
`;
this.querySelectorAll(".priv-level-btn").forEach((btn) => {
btn.addEventListener("click", () => {
const level = (btn as HTMLElement).dataset.level as PrecisionLevel;
this._settings.precision = level;
this._settings.ghostMode = level === "hidden";
this.dispatchEvent(new CustomEvent("precision-change", { detail: level, bubbles: true, composed: true }));
this.render();
});
});
}
}
customElements.define("map-privacy-panel", MapPrivacyPanel);
export { MapPrivacyPanel };