fix: map markers zoom drift — fixed-size container + center anchor

- 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) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-03-27 13:08:36 -04:00
parent 6f901f911c
commit 6d8339fa16

View File

@ -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)
})
}