diff --git a/apps/web/components/charts/district-heatmap.tsx b/apps/web/components/charts/district-heatmap.tsx index 1104ee5..ce747a7 100644 --- a/apps/web/components/charts/district-heatmap.tsx +++ b/apps/web/components/charts/district-heatmap.tsx @@ -160,24 +160,32 @@ export function DistrictHeatmap({ data, city, className, onDistrictClick }: Dist const ratio = point.avgPriceM2 / maxPrice; const size = 36 + ratio * 28; // 36px to 64px + // Mapbox writes `transform: translate(...)` on the marker element; + // hover-scaling the outer element clobbers it. Scale an inner div + // instead. const el = document.createElement('button'); - el.style.cssText = ` - width: ${size}px; height: ${size}px; + el.style.cssText = `width: ${size}px; height: ${size}px; padding: 0; border: none; background: transparent; cursor: pointer;`; + const inner = document.createElement('div'); + inner.style.cssText = ` + width: 100%; height: 100%; border-radius: 50%; border: 2px solid white; background: ${priceColor(ratio)}; - opacity: 0.8; cursor: pointer; + opacity: 0.8; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5); box-shadow: 0 2px 6px rgba(0,0,0,0.3); transition: transform 0.15s, opacity 0.15s; + transform: scale(1); padding: 2px; line-height: 1.1; text-align: center; + pointer-events: none; `; - el.textContent = point.district.replace(/^Quan\s*/i, 'Q.').replace(/^Huyen\s*/i, 'H.'); - el.addEventListener('mouseenter', () => { el.style.opacity = '1'; el.style.transform = 'scale(1.15)'; }); - el.addEventListener('mouseleave', () => { el.style.opacity = '0.8'; el.style.transform = 'scale(1)'; }); + inner.textContent = point.district.replace(/^Quan\s*/i, 'Q.').replace(/^Huyen\s*/i, 'H.'); + el.appendChild(inner); + el.addEventListener('mouseenter', () => { inner.style.opacity = '1'; inner.style.transform = 'scale(1.15)'; }); + el.addEventListener('mouseleave', () => { inner.style.opacity = '0.8'; inner.style.transform = 'scale(1)'; }); el.addEventListener('click', (e) => { e.stopPropagation(); onDistrictClick?.(point.district); diff --git a/apps/web/components/du-an/project-map.tsx b/apps/web/components/du-an/project-map.tsx index 2d415e1..3bbdda2 100644 --- a/apps/web/components/du-an/project-map.tsx +++ b/apps/web/components/du-an/project-map.tsx @@ -80,9 +80,12 @@ export function ProjectMap({ projects, className }: ProjectMapProps) { const bounds = new mapboxgl.LngLatBounds(); geoProjects.forEach((project) => { + // Mapbox owns `transform: translate(...)` on the marker element. + // Apply hover scale to an inner wrapper so we don't clobber it. const el = document.createElement('div'); el.className = 'project-map-marker'; - el.style.cssText = ` + const inner = document.createElement('div'); + inner.style.cssText = ` background: hsl(var(--card)); color: hsl(var(--card-foreground)); border-radius: 8px; @@ -94,16 +97,18 @@ export function ProjectMap({ projects, className }: ProjectMapProps) { cursor: pointer; border-left: 3px solid hsl(var(--primary)); transition: transform 0.15s; + transform: scale(1); max-width: 160px; overflow: hidden; text-overflow: ellipsis; `; - el.textContent = project.name; + inner.textContent = project.name; + el.appendChild(inner); el.addEventListener('mouseenter', () => { - el.style.transform = 'scale(1.05)'; + inner.style.transform = 'scale(1.05)'; }); el.addEventListener('mouseleave', () => { - el.style.transform = 'scale(1)'; + inner.style.transform = 'scale(1)'; }); const statusLabel = PROJECT_STATUS_LABELS[project.status]; diff --git a/apps/web/components/khu-cong-nghiep/park-map.tsx b/apps/web/components/khu-cong-nghiep/park-map.tsx index 005f646..52db958 100644 --- a/apps/web/components/khu-cong-nghiep/park-map.tsx +++ b/apps/web/components/khu-cong-nghiep/park-map.tsx @@ -76,9 +76,12 @@ export function ParkMap({ parks, className }: ParkMapProps) { const bounds = new mapboxgl.LngLatBounds(); geoParks.forEach((park) => { + // Mapbox owns `transform: translate(...)` on the marker element. + // Apply hover scale to an inner wrapper so we don't clobber it. const el = document.createElement('div'); el.className = 'park-map-marker'; - el.style.cssText = ` + const inner = document.createElement('div'); + inner.style.cssText = ` background: hsl(var(--card)); color: hsl(var(--card-foreground)); border-radius: 8px; @@ -90,16 +93,18 @@ export function ParkMap({ parks, className }: ParkMapProps) { cursor: pointer; border-left: 3px solid hsl(var(--primary)); transition: transform 0.15s; + transform: scale(1); max-width: 160px; overflow: hidden; text-overflow: ellipsis; `; - el.textContent = park.name; + inner.textContent = park.name; + el.appendChild(inner); el.addEventListener('mouseenter', () => { - el.style.transform = 'scale(1.05)'; + inner.style.transform = 'scale(1.05)'; }); el.addEventListener('mouseleave', () => { - el.style.transform = 'scale(1)'; + inner.style.transform = 'scale(1)'; }); const statusLabel = PARK_STATUS_LABELS[park.status]; diff --git a/apps/web/components/neighborhood/neighborhood-poi-map.tsx b/apps/web/components/neighborhood/neighborhood-poi-map.tsx index 95326d2..632f5b1 100644 --- a/apps/web/components/neighborhood/neighborhood-poi-map.tsx +++ b/apps/web/components/neighborhood/neighborhood-poi-map.tsx @@ -122,11 +122,20 @@ export function NeighborhoodPOIMap({ visiblePois.forEach((poi) => { const config = POI_CATEGORY_CONFIG[poi.category]; + // Mapbox Marker writes its own `transform: translate(Xpx, Ypx)…` on + // the element it's given. If we mutate `el.style.transform` (e.g. to + // scale on hover), it clobbers the translate and the marker snaps to + // (0, 0). Wrap the visible circle in an INNER div and scale that + // instead, leaving Mapbox's outer transform untouched. const el = document.createElement('div'); el.className = 'poi-marker'; - el.style.cssText = ` - width: 32px; - height: 32px; + el.style.cssText = `width: 32px; height: 32px; cursor: pointer;`; + el.title = `${poi.name} (${config.label})`; + + const inner = document.createElement('div'); + inner.style.cssText = ` + width: 100%; + height: 100%; border-radius: 50%; background: ${config.color}; border: 2px solid white; @@ -134,22 +143,18 @@ export function NeighborhoodPOIMap({ display: flex; align-items: center; justify-content: center; - cursor: pointer; transition: transform 0.15s; + transform: scale(1); + pointer-events: none; `; - el.innerHTML = POI_MARKER_SVG[poi.category]; - el.title = `${poi.name} (${config.label})`; - // Inner SVG would otherwise swallow the click before Mapbox's marker - // handler sees it — mark it as passthrough so pointer events hit the - // wrapping .poi-marker div that Mapbox bound setPopup to. - const innerSvg = el.querySelector('svg'); - if (innerSvg) innerSvg.style.pointerEvents = 'none'; + inner.innerHTML = POI_MARKER_SVG[poi.category]; + el.appendChild(inner); el.addEventListener('mouseenter', () => { - el.style.transform = 'scale(1.3)'; + inner.style.transform = 'scale(1.3)'; }); el.addEventListener('mouseleave', () => { - el.style.transform = 'scale(1)'; + inner.style.transform = 'scale(1)'; }); const popup = new mapboxgl.Popup({ offset: 20, closeButton: true, closeOnClick: true }) diff --git a/apps/web/components/valuation/comparables-map.tsx b/apps/web/components/valuation/comparables-map.tsx index 065c1bf..4924130 100644 --- a/apps/web/components/valuation/comparables-map.tsx +++ b/apps/web/components/valuation/comparables-map.tsx @@ -127,10 +127,14 @@ export function ComparablesMap({ geoComparables.forEach((comp) => { const color = similarityColor(comp.similarity); + // Mapbox owns `transform: translate(...)` on the outer element; + // apply hover scale to an inner wrapper to avoid clobbering it. const el = document.createElement('div'); el.setAttribute('data-testid', 'comparables-map-marker'); - el.style.cssText = ` - background: white; + const inner = document.createElement('div'); + inner.style.cssText = ` + background: hsl(var(--card)); + color: hsl(var(--card-foreground)); border-radius: 8px; padding: 4px 8px; font-size: 11px; @@ -140,16 +144,18 @@ export function ComparablesMap({ cursor: pointer; border-left: 3px solid ${color}; transition: transform 0.15s; + transform: scale(1); max-width: 180px; overflow: hidden; text-overflow: ellipsis; `; - el.textContent = formatPrice(comp.priceVND); + inner.textContent = formatPrice(comp.priceVND); + el.appendChild(inner); el.addEventListener('mouseenter', () => { - el.style.transform = 'scale(1.08)'; + inner.style.transform = 'scale(1.08)'; }); el.addEventListener('mouseleave', () => { - el.style.transform = 'scale(1)'; + inner.style.transform = 'scale(1)'; }); const popup = new mapboxgl.Popup({