From 6d8339fa167dd3c58ec065aa61ff0c87c2ebd3f1 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Fri, 27 Mar 2026 13:08:36 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20map=20markers=20zoom=20drift=20=E2=80=94?= =?UTF-8?q?=20fixed-size=20container=20+=20center=20anchor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - All elements (SVG ring + avatar + badge) inside a fixed-size container (45x45px) with absolute positioning - Avatar centered with calculated offset, ring fills container - Mapbox marker anchor changed from 'bottom' to 'center' - No more variable margins causing offset drift on zoom Co-Authored-By: Claude Opus 4.6 (1M context) --- src/composables/useMap.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/composables/useMap.js b/src/composables/useMap.js index 4e22386..8529f1e 100644 --- a/src/composables/useMap.js +++ b/src/composables/useMap.js @@ -192,24 +192,23 @@ export function useMap (deps) { const donePct = totalHours > 0 ? Math.min(doneHours / 8, 1) : 0 const loadColor = loadPct < 0.5 ? '#10b981' : loadPct < 0.75 ? '#f59e0b' : loadPct < 0.9 ? '#f97316' : '#ef4444' - // SVG ring dimensions - const SIZE = 46, STROKE = 3.5, R = (SIZE - STROKE) / 2 - const CIRC = 2 * Math.PI * R + // Ring + avatar in a fixed-size container so Mapbox anchor stays consistent + const PIN = 36, STROKE = 3.5, SIZE = PIN + STROKE * 2 + 2 // ~45px + const R = (SIZE - STROKE) / 2, CIRC = 2 * Math.PI * R const completedJobs = allToday.filter(j => (j.status || '').toLowerCase() === 'completed').length const totalJobs = allToday.length const completionPct = totalJobs > 0 ? completedJobs / totalJobs : 0 - // Outer wrapper + // Fixed-size outer wrapper — Mapbox anchors to this const outer = document.createElement('div') - outer.style.cssText = 'cursor:pointer;position:relative;' + outer.style.cssText = `cursor:pointer;width:${SIZE}px;height:${SIZE}px;position:relative;` outer.dataset.techId = tech.id - // SVG ring (load arc + completion arc) + // SVG ring (load arc + completion arc) — fills entire container if (totalHours > 0) { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') svg.setAttribute('width', SIZE); svg.setAttribute('height', SIZE) svg.style.cssText = 'position:absolute;top:0;left:0;transform:rotate(-90deg);pointer-events:none;' - // Load arc (how full the day is — faded) const loadArc = document.createElementNS('http://www.w3.org/2000/svg', 'circle') loadArc.setAttribute('cx', SIZE/2); loadArc.setAttribute('cy', SIZE/2); loadArc.setAttribute('r', R) loadArc.setAttribute('fill', 'none'); loadArc.setAttribute('stroke', loadColor) @@ -217,7 +216,6 @@ export function useMap (deps) { loadArc.setAttribute('stroke-dasharray', `${CIRC * loadPct} ${CIRC}`) loadArc.setAttribute('stroke-linecap', 'round') svg.appendChild(loadArc) - // Completion arc (jobs done — solid bright) if (completionPct > 0) { const doneArc = document.createElementNS('http://www.w3.org/2000/svg', 'circle') doneArc.setAttribute('cx', SIZE/2); doneArc.setAttribute('cy', SIZE/2); doneArc.setAttribute('r', R) @@ -230,11 +228,11 @@ export function useMap (deps) { outer.appendChild(svg) } - // Avatar circle (centered inside SVG ring) + // Avatar circle — absolutely centered in container const el = document.createElement('div') el.className = 'sb-map-tech-pin' - const pad = totalHours > 0 ? STROKE + 1 : 0 - el.style.cssText = `background:${color};border-color:${color};margin:${pad}px;` + const offset = (SIZE - PIN) / 2 + el.style.cssText = `background:${color};border-color:${color};position:absolute;top:${offset}px;left:${offset}px;width:${PIN}px;height:${PIN}px;` el.textContent = initials el.title = `${tech.fullName} — ${completedJobs}/${totalJobs} jobs (${doneHours.toFixed(1)}h / ${totalHours.toFixed(1)}h)` outer.appendChild(el) @@ -269,7 +267,7 @@ export function useMap (deps) { el.classList.add('sb-map-gps-active') el.title += ' (GPS)' } - const m = new mbgl.Marker({ element: outer, anchor: 'bottom' }).setLngLat(pos).addTo(map) + const m = new mbgl.Marker({ element: outer, anchor: 'center' }).setLngLat(pos).addTo(map) mapMarkers.value.push(m) }) }