81 lines
3.5 KiB
TypeScript
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;">✓</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 };
|