feat(map): switch to maplibre native#1003
Conversation
✅ Deploy Preview for btcmap ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughMigrate mapping from Leaflet to MapLibre GL across the codebase: update dependencies/types, add basemap/hash/viewport utilities and sprite baking, implement MapLibre controls, rewrite all map components/pages to use GeoJSON sources/layers, remove Leaflet utilities, and update tests/helpers. ChangesLeaflet to MapLibre GL Migration
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Six issues flagged in the Copilot code review, all fixed:
1. AreaMapNext: drop the `geoJSON as GeoJSON.GeoJSON` cast.
`GeoJSON` is a type alias from the geojson package, not a namespace,
so the namespaced reference doesn't compile under strict TS. The
prop is already typed as GeoJSON, no cast needed.
2. map-next hover hull: clear cluster-hull source when convex returns
null. Previously a degenerate cluster (≤ 2 unique points / collinear)
would leave the previous hover's hull on the map until mouseleave.
3. map-next sprite loading: isolate the saved-badge fetch with a catch.
The pin and cluster-hit sprites are local; the saved-badge sprite
fetches the bookmark glyph from iconify.design. A transient Iconify
outage was previously aborting the entire Promise.all and leaving
the map in a half-initialized state with no source/layers.
4-6. Destroyed guards after `await import("maplibre-gl")`:
- map-next/+page.svelte
- components/area/AreaMapNext.svelte
- components/MultiPlaceMapNext.svelte
All three set a `destroyed` flag in onDestroy and bail right after
the dynamic import resolves. Prevents the race where fast navigation
away leaves a Map instance attached to a stale container with no
cleanup path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (9)
src/components/area/AreaMapNext.svelte (2)
80-84: 💤 Low value
tippy()may leak instances on re-runs of the reactive block.The reactive statement re-evaluates whenever
gradeTooltip(or any tracked dep) changes, buttippy([gradeTooltip], {...})creates a fresh instance without destroying the prior one. In practicegradeTooltipis a stablebind:thistarget so this rarely re-runs, but it's worth guarding:♻️ Proposed guarded init
-$: gradeTooltip && - tippy([gradeTooltip], { - content: GradeTable, - allowHTML: true, - }); +let tippyInstance: ReturnType<typeof tippy>[number] | undefined; +$: if (gradeTooltip && !tippyInstance) { + [tippyInstance] = tippy([gradeTooltip], { + content: GradeTable, + allowHTML: true, + }); +} +onDestroy(() => tippyInstance?.destroy());🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/area/AreaMapNext.svelte` around lines 80 - 84, The reactive block that calls tippy([gradeTooltip], { content: GradeTable, allowHTML: true }) can leak tooltip instances on re-runs; modify AreaMapNext.svelte to store the created instance (e.g., let gradeTippy) and before creating a new one check if gradeTippy exists and call gradeTippy.destroy(), then assign the new tippy instance to gradeTippy, and also destroy gradeTippy in onDestroy — keep the same content: GradeTable and allowHTML settings and reference the gradeTooltip bind:this target.
350-369: ⚡ Quick winFont weight inconsistency between styles: ensure
Noto Sans Boldis available in both light and dark map styles.The component uses
Noto Sans Bold(line 358), whilesrc/routes/map-next/+page.svelteusesOpen Sans Semibold. Both AreaMapNext styles reference the same openfreemap glyphs endpoint (https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf). The Liberty style does useNoto Sans Boldin its layers, confirming availability; however, the dark style only referencesNoto Sans Regular, suggesting potential weight inconsistency. Standardize font weights across both styles or verify the dark style actually supports theBoldweight, even if it doesn't use it in base layers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/area/AreaMapNext.svelte` around lines 350 - 369, The layer "comment-badge-count" in AreaMapNext.svelte uses "Noto Sans Bold" while the dark map style only supplies "Noto Sans Regular" (and +page.svelte uses "Open Sans Semibold"), causing weight inconsistency; update the dark style to include the Bold weight at the glyphs source or change the layer's "text-font" to a weight that exists in both styles (e.g., switch "Noto Sans Bold" to a mutually available font like "Open Sans Semibold" or add "Noto Sans Bold" into the dark style's fontstack/glyphs config pointing to https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf), then verify the "comment-badge-count" layer and other layers render consistently in both light and dark styles.src/components/MultiPlaceMapNext.svelte (3)
241-242: 💤 Low value
stopPropagation()onoriginalEventdoes not prevent the bare-map click handler.
e.originalEvent?.stopPropagation?.()only stops DOM propagation; MapLibre dispatches the unscopedclicklistener at line 257 regardless. The bare-map handler is already guarded byqueryRenderedFeatures(...)(line 259), so thestopPropagationline is effectively dead code and can be removed to avoid implying a guarantee it doesn't provide.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/MultiPlaceMapNext.svelte` around lines 241 - 242, Remove the redundant e.originalEvent?.stopPropagation?.(); call in the click handler in MultiPlaceMapNext.svelte — MapLibre still fires the unscoped `click` listener in the bare-map handler, and that handler already guards with `queryRenderedFeatures(...)`, so delete the stopPropagation invocation to avoid implying it prevents the bare-map click; keep the existing `queryRenderedFeatures` guard intact and ensure no other logic expects event propagation to be blocked.
345-359: 💤 Low valueRapid theme toggles can leave overlapping
style.loadhandlers in flight.
applyThemeearly-returns when!styleLoaded, but if the user toggles theme during the brief window betweensetStyleand the nextstyle.load, the second invocation is silently dropped — the map can end up settled on the wrong style. Two options:
- Track a
pendingThemeand re-apply oncestyleLoadedflips back totrue.- Compare
$themeagainstlastAppliedThemeafter eachstyle.loadand re-runapplyThemeif it diverged.In practice the window is small (~tile compile latency), so this is low priority, but worth a note for the polish pass.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/MultiPlaceMapNext.svelte` around lines 345 - 359, applyTheme can drop theme changes when styleLoaded is false because it early-returns, leaving switched themes ignored; change it to record the desired theme (e.g., pendingTheme) before bailing and, in the onStyleLoad handler (registered via map.once("style.load")), compare the current theme/ pendingTheme against lastAppliedTheme and call applyTheme again if they differ (or directly apply the pendingTheme), ensuring map.setStyle and initializeMapContents are retried; alternatively implement the suggested post-load check in onStyleLoad to re-run applyTheme when the active theme diverged from lastAppliedTheme.
89-96: ⚡ Quick winExternal Iconify fetch lacks timeout and runs on every new icon on the render path.
fetch(url)here has noAbortSignal.timeout(...), no retry, and no offline fallback — a stalledapi.iconify.designresponse will leave the pin missing indefinitely (thestyleimagemissingstub stays in place forever). For a third-party CDN dependency on a map's primary visual element, consider:
- Adding
AbortSignal.timeout(5000)(or similar) to thefetchcall.- Logging the failure path so silent
spritePromises.delete(key)inensureSpriteis observable.- Longer term, bundling the small set of category SVGs locally — the dataset has a known, bounded icon vocabulary.
The
MultiPlaceMapuse case is for saved places that all sharecurrency_bitcoin, so it's especially wasteful to depend on a network round trip for a single statically-known glyph here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/MultiPlaceMapNext.svelte` around lines 89 - 96, The fetchIconInnerSvg function currently calls fetch(url) without timeout, retry, fallback, or log, causing missing pins if api.iconify.design stalls; update fetchIconInnerSvg to use AbortSignal.timeout(5000) (or configurable timeout) when calling fetch, add a simple retry (1-2 attempts) and catch to return a local bundled fallback SVG for known category icons (e.g., currency_bitcoin) instead of leaving styleimagemissing, and log failures (include the URL, status or error) before any cleanup like the spritePromises.delete(key) in ensureSprite so failures are observable; keep the icon path resolution via resolveIconifyName and return the fallback text when network fetch ultimately fails.src/routes/map-next/+page.svelte (4)
1056-1056: 💤 Low valueVerify
MerchantDrawerHashimport path from a sibling route.
../map/components/MerchantDrawerHash.svelteresolves into another route directory. SvelteKit treats only+page.*/+layout.*/+server.*/+error.*as route files, so non-prefixed.sveltefiles insrc/routes/map/components/are technically just colocated modules and shouldn't conflict — but using a sibling route's private components from another route is a smell that creates an implicit cross-route coupling. If this is intentional during the incremental migration, consider hoistingMerchantDrawerHashtosrc/components/map/so neither route owns it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/map-next/`+page.svelte at line 1056, The current use of MerchantDrawerHash in +page.svelte imports from a sibling route's module (../map/components/MerchantDrawerHash.svelte), creating an implicit cross-route coupling; to fix, either move/duplicate MerchantDrawerHash into a shared location (e.g., src/components/map/) and update the import in src/routes/map-next/+page.svelte to the new shared path, or explicitly export a stable shared module from a non-route directory and update all consumers to import that shared symbol (MerchantDrawerHash) instead of importing from another route's components folder.
264-265: 💤 Low valueHardcoded
LINK_COLOR = "#0099AF"duplicates the Tailwind palette value.Per the inline comment ("Tailwind
text-linkcolor"), this constant must stay in sync withtailwind.config.js. Two mitigations:
- If using Tailwind v4 (CSS-first config), export the color via a CSS custom property (
--color-link) and read it viagetComputedStyle(document.documentElement).getPropertyValue('--color-link')once at sprite-build time.- Or extract the palette to a small TS module imported by both the config and the component.
Minor — current code works, but drift here means a saved-badge ring that no longer matches the rest of the UI.
Also applies to: 302-315
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/map-next/`+page.svelte around lines 264 - 265, The saved-badge SVG uses a hardcoded LINK_COLOR string which can drift from Tailwind's palette; update buildSavedBadgeSvg (and related usage around lines ~302-315) to obtain the link color dynamically instead of the literal "#0099AF": either read a CSS custom property (e.g., define --color-link in tailwind.config.css and call getComputedStyle(document.documentElement).getPropertyValue('--color-link') once during sprite/build time and pass that value into buildSavedBadgeSvg) or extract the palette into a small shared TS module that both tailwind.config.js and this component import so buildSavedBadgeSvg uses the shared constant; ensure the chosen approach returns a valid color string and replace the LINK_COLOR constant with that dynamic value.
159-180: 💤 Low valueHash writer drops bearing when only pitch is non-zero.
Read the format carefully: if
bearing === 0andpitch !== 0, then:
- Line 170 evaluates
c.bearing !== 0 || c.pitch !== 0→ true → appends/0.0for bearing.- Line 173 then appends
/${pitch}.So the encoded hash becomes
…/lng/0.0/pitch— readable, but on the next parseparts[3] = "0.0"(bearing) andparts[4]is pitch, which round-trips correctly. This is OK. Verified the round-trip is intact.However, a subtle issue:
history.replaceStatefires on everymoveend(line 1013) including programmatic moves triggered bysetView,flyTo, geolocate-tracking updates, and even the initial fitBounds. Firefox/Safari throttlereplaceStateto ~100/30s; rapid pan+rotate gestures can hit that limit and start dropping updates. Consider debouncingpersistViewportToHashto ~250ms like the enrichment trigger.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/map-next/`+page.svelte around lines 159 - 180, The map's writeHashCoords/persistViewportToHash path is called on every moveend (including programmatic setView/flyTo/geolocate updates), causing frequent history.replaceState calls that can be throttled by browsers; debounce persistViewportToHash (e.g., 250ms) so rapid consecutive moveend events collapse into one replaceState. Implement a single debounced wrapper (using a simple setTimeout/clearTimeout or a debounce util) and use that wrapper wherever persistViewportToHash is currently invoked (reference persistViewportToHash, writeHashCoords, and the map 'moveend' listener), ensuring programmatic callers still call the debounced function rather than calling history.replaceState directly.
467-471: 💤 Low valueGeolocate listener and click handlers are not torn down on destroy.
map?.remove()will remove the GL context and stop rendering, and per MapLibre'sMap#removeit does dispose of_listeners— so this is fine for handlers registered viamap.on(...). However:
- The
geolocate.on("geolocate", …)callback (line 593) capturesuserLocation.setLocationand the geolocate control's lifecycle is tied to the map; remove cleans it.triggerEnrichmentIfNeeded.cancel()correctly cancels the in-flight debounce. Good.What's missing: if the cluster-hover async
getClusterLeavescallback (line 962) is in flight at destroy time, it resolves aftermap = undefinedand triesmap.getSource("cluster-hull")indirectly via the resolved hullSource reference. The reference is captured before the await, so it stays valid — buthullSource.setDataon a destroyed map may throw or warn. Consider:source.getClusterLeaves(clusterId, limit, 0).then((leaves) => { + if (destroyed) return; if (latestHullClusterId !== clusterId) return;Also applies to: 1029-1035
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/routes/map-next/`+page.svelte around lines 467 - 471, The geolocate handler and async cluster-leaves callbacks can still run after the map is destroyed; keep a reference to the geolocate listener function and unregister it (call geolocate.off("geolocate", handler) or remove the control) during teardown, and also guard or cancel any in-flight cluster fetches so they don't call hullSource.setData on a destroyed map: for the cluster-hover async getClusterLeaves callback, use an AbortController or check the same destroyed flag (or map truthiness) right before calling hullSource.setData (and skip if destroyed), and ensure any click handlers registered via map.on(...) are removed with map.off(...) in the destroy path; refer to the geolocate.on("geolocate", ...) handler, the getClusterLeaves async callback that calls hullSource.setData, and triggerEnrichmentIfNeeded.cancel() when implementing these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/area/AreaMapNext.svelte`:
- Around line 510-518: The grade currently always computes to 100% because both
total and upToDate are set to filteredPlaces.length; change the logic so total
reflects the full set of places (pre-verification) and upToDate reflects the
verified/filtered count: leave upToDate = filteredPlaces.length but set total to
the unfiltered places collection (e.g. the variable that contains allPlaces or
places before AreaPage filtering), compute upToDatePercent using (upToDate /
total) * 100 with a guard for total === 0, and then call grade =
getGrade(Number(upToDatePercent.toFixed(0))) so getGrade receives the correct
percentage.
In `@src/components/MultiPlaceMapNext.svelte`:
- Around line 59-128: This file duplicates the pin-sprite pipeline
(materialExceptions, resolveIconifyName, PIN_PATH, PIN_FILL_REGULAR, spriteName,
fetchIconInnerSvg, buildCompositeSvg, loadSvgImage, ensureSprite and
transparentPixel) that exists in AreaMapNext.svelte and map-next/+page.svelte;
extract these helpers into a new module (e.g. $lib/maplibre/pinSprites.ts) and
export resolveIconifyName, spriteName(icon, boosted?), ensureSprite(map, icon,
boosted?), and installPlaceholderHandler(map). Replace the in-file definitions
in MultiPlaceMapNext.svelte with imports of those functions and update local
calls (resolveIconifyName, spriteName, ensureSprite, buildCompositeSvg,
fetchIconInnerSvg, loadSvgImage) to use the exported APIs, ensuring the map-next
variant’s fallback behavior and AreaMapNext’s boosted variant are supported by
adding optional boosted and fallback parameters in the new module so callers can
pass through their existing semantics.
In `@src/routes/map-next/`+page.svelte:
- Around line 721-727: The MapLibre style expression assumes boosted is non-null
and will throw; update the expression used in the icon-image concat to coalesce
boosted to a boolean (mirror the AreaMapNext.svelte pattern) by replacing
["case", ["get", "boosted"], "b", "r"] with ["case", ["coalesce", ["get",
"boosted"], false], "b", "r"]; apply the identical defensive change wherever the
same expression is used (notably in the spiderLeavesLayout-generated leaf
features) and keep buildFeatureCollection untouched but consistent with this
coalesce pattern.
- Around line 482-548: The style currently points glyphs to
"https://demotiles.maplibre.org/..." and uses raw OpenStreetMap tiles in the
"osm" source (and public CARTO tiles in "carto-light"/"carto-dark"), which are
demo/free-tier endpoints unsuitable for production; update the glyphs property
to point to your self-hosted or paid glyph service (e.g., PMTiles/protomaps on
static.btcmap.org or a vendor), replace the "osm" source tiles array with your
self-hosted tile endpoint or a paid tile vendor (or Stadia/MapTiler/Protomaps)
and confirm "carto-light"/"carto-dark" usage complies with CARTO's terms or swap
them to a supported provider, keeping the existing layer ids ("osm",
"carto-light", "carto-dark") and layout visibility logic intact (no changes to
initialBasemap checks required).
---
Nitpick comments:
In `@src/components/area/AreaMapNext.svelte`:
- Around line 80-84: The reactive block that calls tippy([gradeTooltip], {
content: GradeTable, allowHTML: true }) can leak tooltip instances on re-runs;
modify AreaMapNext.svelte to store the created instance (e.g., let gradeTippy)
and before creating a new one check if gradeTippy exists and call
gradeTippy.destroy(), then assign the new tippy instance to gradeTippy, and also
destroy gradeTippy in onDestroy — keep the same content: GradeTable and
allowHTML settings and reference the gradeTooltip bind:this target.
- Around line 350-369: The layer "comment-badge-count" in AreaMapNext.svelte
uses "Noto Sans Bold" while the dark map style only supplies "Noto Sans Regular"
(and +page.svelte uses "Open Sans Semibold"), causing weight inconsistency;
update the dark style to include the Bold weight at the glyphs source or change
the layer's "text-font" to a weight that exists in both styles (e.g., switch
"Noto Sans Bold" to a mutually available font like "Open Sans Semibold" or add
"Noto Sans Bold" into the dark style's fontstack/glyphs config pointing to
https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf), then verify the
"comment-badge-count" layer and other layers render consistently in both light
and dark styles.
In `@src/components/MultiPlaceMapNext.svelte`:
- Around line 241-242: Remove the redundant
e.originalEvent?.stopPropagation?.(); call in the click handler in
MultiPlaceMapNext.svelte — MapLibre still fires the unscoped `click` listener in
the bare-map handler, and that handler already guards with
`queryRenderedFeatures(...)`, so delete the stopPropagation invocation to avoid
implying it prevents the bare-map click; keep the existing
`queryRenderedFeatures` guard intact and ensure no other logic expects event
propagation to be blocked.
- Around line 345-359: applyTheme can drop theme changes when styleLoaded is
false because it early-returns, leaving switched themes ignored; change it to
record the desired theme (e.g., pendingTheme) before bailing and, in the
onStyleLoad handler (registered via map.once("style.load")), compare the current
theme/ pendingTheme against lastAppliedTheme and call applyTheme again if they
differ (or directly apply the pendingTheme), ensuring map.setStyle and
initializeMapContents are retried; alternatively implement the suggested
post-load check in onStyleLoad to re-run applyTheme when the active theme
diverged from lastAppliedTheme.
- Around line 89-96: The fetchIconInnerSvg function currently calls fetch(url)
without timeout, retry, fallback, or log, causing missing pins if
api.iconify.design stalls; update fetchIconInnerSvg to use
AbortSignal.timeout(5000) (or configurable timeout) when calling fetch, add a
simple retry (1-2 attempts) and catch to return a local bundled fallback SVG for
known category icons (e.g., currency_bitcoin) instead of leaving
styleimagemissing, and log failures (include the URL, status or error) before
any cleanup like the spritePromises.delete(key) in ensureSprite so failures are
observable; keep the icon path resolution via resolveIconifyName and return the
fallback text when network fetch ultimately fails.
In `@src/routes/map-next/`+page.svelte:
- Line 1056: The current use of MerchantDrawerHash in +page.svelte imports from
a sibling route's module (../map/components/MerchantDrawerHash.svelte), creating
an implicit cross-route coupling; to fix, either move/duplicate
MerchantDrawerHash into a shared location (e.g., src/components/map/) and update
the import in src/routes/map-next/+page.svelte to the new shared path, or
explicitly export a stable shared module from a non-route directory and update
all consumers to import that shared symbol (MerchantDrawerHash) instead of
importing from another route's components folder.
- Around line 264-265: The saved-badge SVG uses a hardcoded LINK_COLOR string
which can drift from Tailwind's palette; update buildSavedBadgeSvg (and related
usage around lines ~302-315) to obtain the link color dynamically instead of the
literal "#0099AF": either read a CSS custom property (e.g., define --color-link
in tailwind.config.css and call
getComputedStyle(document.documentElement).getPropertyValue('--color-link') once
during sprite/build time and pass that value into buildSavedBadgeSvg) or extract
the palette into a small shared TS module that both tailwind.config.js and this
component import so buildSavedBadgeSvg uses the shared constant; ensure the
chosen approach returns a valid color string and replace the LINK_COLOR constant
with that dynamic value.
- Around line 159-180: The map's writeHashCoords/persistViewportToHash path is
called on every moveend (including programmatic setView/flyTo/geolocate
updates), causing frequent history.replaceState calls that can be throttled by
browsers; debounce persistViewportToHash (e.g., 250ms) so rapid consecutive
moveend events collapse into one replaceState. Implement a single debounced
wrapper (using a simple setTimeout/clearTimeout or a debounce util) and use that
wrapper wherever persistViewportToHash is currently invoked (reference
persistViewportToHash, writeHashCoords, and the map 'moveend' listener),
ensuring programmatic callers still call the debounced function rather than
calling history.replaceState directly.
- Around line 467-471: The geolocate handler and async cluster-leaves callbacks
can still run after the map is destroyed; keep a reference to the geolocate
listener function and unregister it (call geolocate.off("geolocate", handler) or
remove the control) during teardown, and also guard or cancel any in-flight
cluster fetches so they don't call hullSource.setData on a destroyed map: for
the cluster-hover async getClusterLeaves callback, use an AbortController or
check the same destroyed flag (or map truthiness) right before calling
hullSource.setData (and skip if destroyed), and ensure any click handlers
registered via map.on(...) are removed with map.off(...) in the destroy path;
refer to the geolocate.on("geolocate", ...) handler, the getClusterLeaves async
callback that calls hullSource.setData, and triggerEnrichmentIfNeeded.cancel()
when implementing these changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: db99e84b-a5a0-4a24-b3b3-2a91d08fe80d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
package.jsonsrc/components/MultiPlaceMapNext.sveltesrc/components/area/AreaMapNext.sveltesrc/routes/+layout.sveltesrc/routes/map-next/+page.svelte
Six issues flagged in the Copilot code review, all fixed:
1. AreaMapNext: drop the `geoJSON as GeoJSON.GeoJSON` cast.
`GeoJSON` is a type alias from the geojson package, not a namespace,
so the namespaced reference doesn't compile under strict TS. The
prop is already typed as GeoJSON, no cast needed.
2. map-next hover hull: clear cluster-hull source when convex returns
null. Previously a degenerate cluster (≤ 2 unique points / collinear)
would leave the previous hover's hull on the map until mouseleave.
3. map-next sprite loading: isolate the saved-badge fetch with a catch.
The pin and cluster-hit sprites are local; the saved-badge sprite
fetches the bookmark glyph from iconify.design. A transient Iconify
outage was previously aborting the entire Promise.all and leaving
the map in a half-initialized state with no source/layers.
4-6. Destroyed guards after `await import("maplibre-gl")`:
- map-next/+page.svelte
- components/area/AreaMapNext.svelte
- components/MultiPlaceMapNext.svelte
All three set a `destroyed` flag in onDestroy and bail right after
the dynamic import resolves. Prevents the race where fast navigation
away leaves a Map instance attached to a stale container with no
cleanup path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ebae51c to
cc2cea9
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Looks and feels great. Rotate and pan on mobile work well. I also like the more granular zoom levels all round. And the merchant deep links on the map are super cool too! Feedback:
|
🙌
Which ones? I assume everything is like before? :D
Will check, I assume currently everything as it comes default OOTB
Should be same as it is on prod currently. But can be adjusted.
You mean that select or that light/dark-theme switcher? Sound like this is mainly fine. Will add missing changes step by step via follow up PRs into this one. |
|
Maybe rotations on desktop is user error. If you use mouse left/right, rather than mouse up/down it's fine. |
This comment was marked as outdated.
This comment was marked as outdated.
Phase 0 of the Leaflet → MapLibre native migration. Adds an isolated /map-next route alongside the existing /map, rendering an OSM raster basemap with NavigationControl (zoom + compass) and native two-finger rotation enabled. No markers, no clustering — just the foundation. Verified under rotation: tiles fill the viewport correctly at any bearing, which was the blocking Risk F that killed the leaflet-rotate spike. Vector basemaps come in Phase 4. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subscribe to $places store, convert to GeoJSON, and render through a clustered MapLibre source with three layers (outer disc, inner disc, count symbol) matching leaflet.markercluster default styling, plus a placeholder circle layer for unclustered points. Sprites land in Phase 2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small fixes needed to make Phase 1 visible end-to-end: 1. Add `/map-next` to the fullscreen route allowlist in +layout.svelte so the site header/nav doesn't render on top of the map. 2. Add a `glyphs:` URL to the inline style spec and a `text-font` to the cluster-count symbol layer. MapLibre requires both for any symbol layer with `text-field`. Using the official MapLibre demo glyph server for now; Phase 4 swaps to vector basemaps that ship their own glyphs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First cut of Phase 2. Replaces the placeholder orange dots with the existing /icons/div-icon-pin.svg (teal) and /icons/boosted-icon-pin.svg (orange) loaded via map.addImage and rendered through a symbol layer. Boost state is derived from boosted_until via the existing isBoosted utility, then projected into the feature property `boosted` so the symbol layer can pick the right sprite via a 'case' expression. Pins are anchored at bottom (so the point tip sits on the coordinate) and pinned to the viewport for rotation/pitch — they stay upright as the map turns. Category icons inside the pin, comment-count badges, saved-state overlay, and boost-glow animation are still to come within Phase 2. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate composite pin sprites at runtime — each unique (icon, boosted) pair fetches its Material icon from the Iconify static API, composes it inside the 32x43 pin shape, and registers it via map.addImage. The unclustered-point symbol layer now resolves icon-image dynamically by concatenating the place's icon and boosted flag. Sprite generation is deduped via a module-scoped promise cache and triggered on every places sync; failed fetches are evicted so they can retry on the next tick. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two symbol layers above the composite pin: - `comment-badge`: green disc with count at pin top-right, using text-halo as the disc background so 1- to 3-digit counts stay legible without an extra sprite. - `saved-badge`: white disc with bookmark glyph at pin top-left, pre-rendered into a 16x16 sprite at map load. Extends GeoJSON feature properties with `comments` and `saved`, and adds a `$savedPlaceIds`-size trigger so the source rebuilds when the user toggles saves. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use supercluster's getClusterExpansionZoom to ease the camera into a cluster on click. Pointer cursor on cluster and pin hover gives the usual click affordance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Click an unclustered pin → call merchantDrawer.open(id, "details"). The drawer is already mounted at the layout level and reacts to store state, so no extra wiring is needed here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add @nazka/map-gl-js-spiderfy and apply it to an invisible "clusters-hit" symbol layer. The library auto-routes click events: if a cluster can still split via getClusterExpansionZoom, it eases the camera; if not (e.g. multiple places at the same coords at max cluster zoom), it spiderfies the leaves into a circle/spiral. Leaves use our pin-sprite icon-image expression, and onLeafClick forwards to merchantDrawer.open. Also pre-install @turf/convex and @turf/helpers — used in the next commit for the cluster-hover convex hull. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On mouseenter of a cluster, fetch its leaves (capped at 500), build a @turf/convex polygon and push it into a dedicated cluster-hull GeoJSON source. The fill + outline layers sit below the cluster discs so the soft green tint reads as a footprint, not an overlay. mouseleave clears the source. A latestHullClusterId guard discards stale leaf callbacks when the cursor sweeps between clusters faster than getClusterLeaves resolves. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…awer The Phase 3 click handler calls merchantDrawer.open() but the actual drawer component lives in /map and is only mounted there. Without mounting it on /map-next, the store updates land in the void and no UI appears. Reuse the existing MerchantDrawerHash component from ../map/components/ — both routes share state via the global merchantDrawer store anyway, and the component handles desktop/mobile switching internally. This stays in /map/components/ for now rather than being moved to a shared location, because both routes coexist until the Phase 7 cutover; moving it would force a refactor of the /map page mid- migration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ad of always spidering The @nazka spiderfy library's internal decision logic is: expansionZoom > forceSpiderifyMinZoom || expansionZoom > maxZoom → spiderfy else → easeTo(expansionZoom) Default forceSpiderifyMinZoom is null. In JavaScript, `r > null` evaluates as `r > 0`, which is always true for any cluster's expansion zoom — so the default behavior is to spider every cluster click, regardless of zoom level. That's why a normal cluster at zoom 3 was spidering out 150+ pins instead of zooming in. Setting forceSpiderifyMinZoom to CLUSTERING_DISABLED_ZOOM (17) means: - expansionZoom ≤ 17 → easeTo (normal cluster zoom-in) - expansionZoom > 17 → spiderfy (only for coincident points that can't be broken apart by zooming, e.g. multiple places at the same lat/lon) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…halo Previously the comment count badge used text-halo-width as the disc background. That looked off at single-digit counts because the halo hugs the glyph, so "1" rendered noticeably smaller than prod's flat 16px disc. Switch to two stacked symbol layers sharing one offset: - comment-badge (icon symbol, comment-badge-bg sprite) - comment-badge-count (text symbol, count rendered on top) The disc sprite is now drawn directly on a canvas (no SVG roundtrip) since it's just a flat circle. Text bumped 10 → 11 px for legibility. Open Sans Semibold is the weight; demotiles glyphs don't ship a Bold variant, so Phase 4's vector basemap can later bump weight if needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port `/map`'s viewport-bound enrichment pattern to the MapLibre-native route: at zoom >= LABEL_VISIBLE_ZOOM, a debounced moveend handler calls merchantList.fetchEnrichedDetails(), and a new `place-label` symbol layer reads each feature's resolved name (enriched-cache > $places.name > osm:amenity) from the GeoJSON properties. Labels appear progressively as the cache fills. The reactive rebuild block now also triggers on placeDetailsCache size changes (same size-tracker pattern as $places/savedPlaceIds; same known swap-edge limitation). Styling mirrors prod's `.marker-label` / `.marker-label-boosted` (cyan-700 / orange-500, white halo). Dark mode and localized_name are deferred to Phase 5/6. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Default to OpenFreeMap Liberty (light) / static.btcmap.org dark style (dark) so the page no longer ships with the inline OSM raster style. - Add a typed BASEMAPS catalog (5 entries: 2× OpenFreeMap, 2× Carto, OSM raster fallback) and a top-right <select> switcher; the user's pick persists via localStorage(btcmap-basemap). - Refactor the source/layer setup into addPlacesSourceAndLayers and the sprite registration into loadAllSprites so they can re-run on every style.load (setStyle wipes sources, layers, and the image registry). Spiderfy / click / hover handlers stay set up once on the initial map.load. - Clear the composite-pin spritePromises cache on style.load so ensureSpritesForPlaces rebuilds the per-icon sprites against the fresh image registry. - Widen text-font on cluster-count, comment-badge-count, and place-label to ["Open Sans Semibold", "Noto Sans Regular"] — the new vector basemaps may not ship Open Sans, so we fall back to the Noto stack they all include. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pin sprites were rasterized at 2× the declared 32×43 px and registered with pixelRatio: 2. Sharp on 2× retina monitors, but most modern phones report DPR 3 (or 4), so MapLibre upscaled the 64×86 bitmap ~50% for native rendering — visibly soft category glyphs. Bump PIN_RENDER_SCALE from 2 → 3. Sprite cache doubles in memory but the category-icon set in view at any time is bounded (low dozens), so the impact is tens of KB at most. The composite SVG and inner Iconify fetch are unchanged; only the rasterization resolution + pixelRatio flag at addImage time shift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The green disc behind each pin's comment-count number was drawn into a 16×16 canvas and registered with pixelRatio: 1. On retina displays MapLibre upscaled the 16×16 bitmap to 32×32 native pixels — soft edges, visible "bitmap blur" on the disc. Bump the canvas to 32×32 (2× SIZE) and register with pixelRatio: 2. Display size unchanged at 16 logical px; pixel data now matches retina 1:1. Same change applied to /merchant/[id] and AreaMap (each has its own copy of loadCommentBadgeSprite). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The white-disc + bookmark badge on saved places was assembled as a 16×16 SVG composite and registered with pixelRatio: 1. Same blur on retina as the comment badge had. Bump the composite SVG's width/height to 2× (viewBox unchanged) and register with pixelRatio: 2. The inner Iconify bookmark glyph re-rasterizes at the new resolution because nested SVG content scales with the outer's bitmap size. Display dimensions unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
A more general thoughts was around any impact to apps that have integrated via WebView/iFrame. Here's what GPT5.5 said:
What do you think? We could 'test in live' for known integrations and quicky fallback if we see issues? |
Legacy /map read ?lat=X&long=Y for initial center and supported two pairs as a fitBounds rectangle. Embeds linking with those params would land on the default viewport after the MapLibre rewrite. Add a parseLatLongQuery helper (with unit tests) and resolve the initial viewport in this order: hash → ?lat&long → defaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1003 Legacy /map used maxZoom: 21. The MapLibre rewrite tightened it to 19, which silently clamped deep-linked zooms above that level. OpenFreeMap Liberty serves vector tiles that overzoom cleanly to 21+, and Carto raster tiles upscale at the worst case — same behavior the prod /map already had. Applied to /map, AreaMap, and MultiPlaceMap. Geolocate / fitBounds caps (maxZoom: 15) intentionally left unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1003 Leaflet rendered tiles to a DOM/canvas-2d pane that worked in any modern browser. MapLibre requires WebGL, so older Android WebViews, hardened browsers, restricted enterprise installs, and devices with hardware acceleration disabled would leave the map container empty. Add a small hasWebGL() helper that probes for webgl2/webgl/ experimental-webgl, and a shared MapUnsupportedFallback component that fills the map's container slot with an explanatory message. Each of the six MapLibre instantiation sites (/map, /merchant/[id], /communities/map, /add-location, AreaMap, MultiPlaceMap) checks hasWebGL() before instantiating and renders the fallback otherwise. On /map also suppress the tile-loading chip when the fallback is up — the indicator would otherwise sit stuck at "loading…" forever. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sites #1003 Follow-up to 28d47a0: the previous commit only updated /map, AreaMap, and MultiPlaceMap. /merchant/[id], /add-location, and /communities/map were still clamped to 19. Bumping them too so the entire app accepts deep-linked street-level zooms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1003 Legacy /map set `noWrap: true` on the raster OSM layer so panning west past the antimeridian didn't reveal a duplicate copy of the world. MapLibre defaults to renderWorldCopies: true, so the rewrite was showing repeated copies at low zoom. Match the legacy contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map subscribed to \$placesError and surfaced sync errors via errToast so the user didn't sit on an empty map without explanation (line 401 of the Leaflet version). The MapLibre rewrite dropped that subscription entirely. Wire it back. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map called setView(..., 17) for single search hits, ensuring the pin landed at a zoom past CLUSTERING_DISABLED_ZOOM (17) so it rendered as a standalone marker. The MapLibre rewrite used DEFAULT_MAP_ZOOM (15), where the matched place can sit inside a cluster disc — the user searches for a merchant and sees a cluster instead of their pin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map registered a Leaflet L.control.scale via scaleBars(). The MapLibre rewrite never replaced it, so users lost the visual distance reference. ScaleControl is built into MapLibre — one addControl call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+page.server.ts already populated data.geo from Netlify's x-nf-geo header, but +page.svelte never read it after the MapLibre rewrite, so visitors with no URL state always started on Curaçao (the project default). Legacy /map consulted data.geo as the next fallback after hash/query — restore that behavior. Viewport resolution chain is now: hash → ?lat&long → IP-geo → defaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map injected a "Support BTC Map" anchor into Leaflet's attribution control via DOM mutation (lib/map/setup.ts:support()), so every map view exposed an entry point to the supporters page. The MapLibre rewrite dropped this entirely. Inline the link into each source's attribution string in the style spec — MapLibre's default AttributionControl reads from there, so no post-init DOM tampering needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BasemapsControl.ts:139 references mapControls.basemapTitle for the basemap-picker button tooltip, but the key didn't exist in any locale. Non-EN users saw the literal key string. Add proper translations to all 8 locales — keeps the i18n contract aligned per CLAUDE.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an embed links to /map with bad ?lat or ?long values (NaN, out-of-range, asymmetric counts that wouldn't parse), the rewrite silently falls through to defaults — no hint that the URL was the problem. Legacy /map showed errors.mapView in this case. Match it by detecting "has lat/long params but parseLatLongQuery returned null" and toasting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
updateSinglePlace() in \$lib/sync/places.ts mutates a place in-place and sets lastUpdatedPlaceId, but the source-sync guard in /map only re-renders on length / size / locale changes — none of which fire for an in-place mutation. The boosted-pin orange fill and the comment-count badge wouldn't appear until the next full sync. Add a parallel reactive that listens for lastUpdatedPlaceId and re-syncs the source, then clears the store. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map called clearNonSearchResultMarkers() + loadSearchResultMarkers() when the user ran a search, so the map only showed the matched pins — matching the panel list. The MapLibre rewrite filtered the list but not the map. Users would search for a merchant and see thousands of unrelated pins surrounding it. Compute an "effective" pin list inside the existing source-sync reactive: search mode with results → matched places; otherwise → all \$places. Track a searchSig (joined result ids) alongside the existing length / size trackers so a new search with the same hit count still triggers a re-sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MapLibre rewrite dropped a handful of map controls and changed the loading-status priority chain, leaving these keys unreferenced in all 8 locales: mapControls.fullScreen, mapControls.fullScreenAlt — fullscreen toggle removed from the control bar. mapControls.support, mapControls.supportWithSats — legacy injected a "Support BTC Map" link via DOM mutation; rewrite bakes the link into the attribution string directly, no separate i18n entry needed. errors.fullscreenError, errors.mapViewCachedCoords — orphaned with the localforage coords cache and fullscreen toggle. status.loadingPlaces, status.initializingMarkers, status.loadingPlacesInView — superseded by the new priority chain (syncing → preparing → tiles). Verified each is unreferenced before deletion (errors.mapView kept — it's used by /communities/map and by the new bad-coords toast). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#1003 Legacy /map tracked zoom_out_click, locate_click, and layer_change events alongside the existing zoom_in_click. The MapLibre rewrite only kept zoom_in_click, leaving a gap in map-UI telemetry. MapLibre's built-in NavigationControl and GeolocateControl don't expose JS-level click events, so attach DOM listeners to the buttons in the container after addControl. BasemapsControl is our own IControl — wire trackEvent directly inside #select. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map called clearNonMatchingMarkers(category) whenever the user changed the panel's category filter, so the map and the list stayed in sync. The MapLibre rewrite only filtered the list, leaving every pin visible. Selecting "Restaurants" showed exactly the same pin cloud as "All". merchantListStore already exposes selectedCategory; categoryMapping already exports filterMerchantsByCategory. Plug both into the existing source-sync reactive between the search and full-list branches — search and category are mutually exclusive (the store resets the category when entering search mode), so the precedence chain is in-search → category != "all" → all places. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Legacy /map used localforage to cache the map bounds on every move
(debounced 1s) and restored them on the next visit. Returning users
landed back where they left off without needing a URL hash. The
MapLibre rewrite dropped the integration entirely, so every visit
started at the project default (Curaçao) or the IP-geo guess.
Restore it via a small viewportCache helper:
- loadCachedView() returns {lat, lng, zoom} or null, validated.
- saveCachedView({lat, lng, zoom}) writes through localforage,
swallows errors (quota / IndexedDB unavailable).
- Versioned key (coords-next-v1) so a stale prod-Leaflet build's
Leaflet LatLngBounds blob doesn't poison this branch.
Resolution chain is now hash → query → cached view → IP-geo →
defaults, and a debounced (1s) saveCachedView fires alongside the
existing persistViewportToHash on every moveend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MapLibre's bundled CSS hardcodes a white background on .maplibregl-ctrl-group, so the rounded pill that holds zoom / locate / nav / boost / data-refresh / basemap controls read as glaring white blocks against any dark basemap or dark-mode UI. Legacy /map and /communities/map handled the equivalent .leaflet-bar via explicit dark-mode rules; the MapLibre rewrite never carried those over. Add .dark variants to controls.css for the pill background, hover state, separator borders, and an invert(1) filter on the icon glyphs (MapLibre stock icons are dark-gray background-images on a child <span>; our custom controls use <img> children — both are single-color silhouettes that invert cleanly). controls.css is already imported by all custom IControls so the rules ship on /map and /communities/map. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ort #1003 The default MapLibre popup styling on /communities/map looked broken — white box even in dark mode, the close X squeezed inside the content padding so it sat awkwardly next to the community avatar, no real border or shadow. Add a scoped communities-map.css with: - Comfortable padding, border-radius, drop shadow. - Close button repositioned to absolute top-right with its own square hit area and hover state. - Dark-mode popup body (#06171c), text color, and a re-colored tip triangle (CSS triangle built from border-color, so all four sides are overridden to handle every anchor direction). Scoped to .communities-map-page on the route wrapper so /map's merchant drawer isn't affected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four EventName union members weren't doing anything useful: - fullscreen_click — no caller. The fullscreen toggle was removed in the MapLibre rewrite (decision was to skip re-implementing it), so post-merge this event has zero emitters. Dashboard still shows 574/30d coming from prod-Leaflet users — that'll drain to zero. - nearby_button_click — no caller anywhere, never appeared in the dashboard either. Type-only ghost from an earlier refactor. - zoom_in_click / zoom_out_click — high volume (~10k/30d combined) but the data isn't actionable. Users zoom; that's a given. The signal didn't change any decision and was 12% of the event budget for no insight. Remove both the union members and the two trackEvent callers (zoomToNearbyLevel for zoom_in, the post-addControl DOM listener for zoom_out). locate_click stays — it's a meaningful interaction signal distinct from the user just panning the map. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1003 Two small dark-mode polish misses from the previous CSS commits: 1. BasemapsControl renders an inline svg with stroke=currentColor as its layer-stack icon, but my invert filter selector only caught the <img> children. The svg stayed dark-on-dark in dark mode. Add a matching selector so the layer button looks like the others. 2. The community-map popup tip is a CSS triangle built from border colors: three sides transparent, one colored to make the visible face. Painting all four borders with the dark bg collapsed the triangle into a square — the arrow disappeared. Replace the blanket override with per-anchor selectors that mirror MapLibres stock rules (border-bottom for anchor-top variants, border-top for anchor-bottom variants, etc.) so only the visible face gets recolored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ross the tip #1003 The subtle white-08 border I added on the popup body sat on top of the CSS triangle that forms the tip arrow — the popup-content's bottom border drew a horizontal line across the base of the triangle, making the arrow look notched. The box-shadow already provides enough separation between the popup body and the dark basemap, so the border isn't earning its keep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>


Re-writing all ze maps to solve #996 🤞
?lat=&long=(single point + bounds-pair) still works; deep-linked street-level zoom (21) works again everywhereSummary by CodeRabbit
New Features
Refactor
Bug Fixes
Tests