Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
1864de4
feat(map-next): scaffold /map-next route with bare MapLibre map
escapedcat May 13, 2026
aed7437
feat(map-next): add clustered places layer
escapedcat May 13, 2026
f17104b
fix(map-next): hide site header and wire glyph server for cluster counts
escapedcat May 13, 2026
fc99fb0
feat(map-next): use pin SVG sprites for unclustered markers
escapedcat May 13, 2026
f8102e9
feat(map-next): bake category icon into pin sprites
escapedcat May 13, 2026
aead1b0
feat(map-next): add comment count and saved overlay badges
escapedcat May 13, 2026
5c55935
feat(map-next): zoom cluster on click and add pointer cursors
escapedcat May 13, 2026
8c1fb74
feat(map-next): open merchant drawer on marker click
escapedcat May 13, 2026
2a6f938
feat(map-next): wire up spiderfy for tightly-packed clusters
escapedcat May 13, 2026
d8c6967
feat(map-next): draw convex hull on cluster hover
escapedcat May 13, 2026
f4920e5
feat(map-next): mount MerchantDrawerHash so marker clicks open the dr…
escapedcat May 13, 2026
ad08ce0
fix(map-next): set forceSpiderifyMinZoom so cluster click zooms inste…
escapedcat May 13, 2026
e01170e
fix(map-next): give comment badge a fixed 16x16 disc instead of text-…
escapedcat May 13, 2026
510ee58
feat(map-next): show place name labels at high zoom via lazy enrichment
escapedcat May 13, 2026
fce8252
feat(map-next): switch to vector basemap with selector
escapedcat May 13, 2026
48f3879
Revert "feat(map-next): switch to vector basemap with selector"
escapedcat May 13, 2026
eff1251
feat(map-next): add geolocate control and sync to userLocation store
escapedcat May 13, 2026
452144c
feat(map-next): sync viewport to URL hash with bearing/pitch support
escapedcat May 13, 2026
11b6981
feat(map-next): resolve localized place names by current i18n locale
escapedcat May 13, 2026
3514922
feat(area): add MapLibre-native AreaMapNext component
escapedcat May 13, 2026
074fbe1
feat(components): add MapLibre-native MultiPlaceMapNext component
escapedcat May 13, 2026
7a835ec
fix(map-next): cascade icon fallback so all category pins render
escapedcat May 13, 2026
46afcf4
fix(map-next): close drawer when clicking empty map background
escapedcat May 13, 2026
2a256b9
feat(map-next): add basemap switcher (OSM / Carto Light / Carto Dark)
escapedcat May 13, 2026
aa39af5
fix(map-next): address Copilot review findings on PR #1003
escapedcat May 13, 2026
da22725
refactor(map): extract pin-sprite pipeline into shared module
escapedcat May 13, 2026
0f0344d
fix(map-next): coalesce boosted/icon expressions in pin layers
escapedcat May 13, 2026
8300606
fix(map-next): switch glyphs from demotiles to OpenFreeMap
escapedcat May 13, 2026
1b8568d
fix(map-next): self-review blocker fixes for beta readiness
escapedcat May 13, 2026
9c7df88
fix(maplibreSprites): render pin sprites at 2Γ— for retina-crisp icons
escapedcat May 13, 2026
5d42098
refactor(icons): extract materialExceptions into shared module
escapedcat May 14, 2026
4efb585
refactor(map-next): extract hash, basemap, and radius helpers
escapedcat May 14, 2026
7532b4e
fix(map-next): move basemap switcher below the control stack
escapedcat May 14, 2026
b7f9571
feat(map-next): add globe projection toggle
dadofsambonzuki May 15, 2026
49da126
fix(map-next): push basemap switcher below the v5 control stack
escapedcat May 15, 2026
28f643c
fix(map-next): basemap switcher follows dark mode
escapedcat May 15, 2026
61d43c0
fix(map-next): place-name labels follow theme
escapedcat May 15, 2026
8c5bc27
feat(map-next): mount merchant list panel
escapedcat May 15, 2026
6882044
feat(map-next): mount floating MapSearchBar
escapedcat May 15, 2026
b2afcec
feat(map-next): add nav/boost/refresh map controls
escapedcat May 15, 2026
3bc02cc
feat(map-next): basemap picker as custom IControl
escapedcat May 15, 2026
3cb2568
fix(map): solid hover bg for search-bar tabs in dark mode
escapedcat May 24, 2026
80c236a
feat(map-next): port CommunityRail to MapLibre
escapedcat May 24, 2026
0773d13
feat(map-next): wire TileLoadingIndicator
escapedcat May 24, 2026
eb1b4d8
feat(map-next): show loading indicators on initial map load
escapedcat May 24, 2026
0468a0e
refactor(area): swap AreaMap β†’ AreaMapNext
escapedcat May 24, 2026
cf327c7
refactor(saved): swap MultiPlaceMap β†’ MultiPlaceMapNext
escapedcat May 24, 2026
0cd1613
feat(map): cut over /map to MapLibre, remove Leaflet route
escapedcat May 24, 2026
3ed2b87
refactor(map): remove dead Leaflet helpers and orphaned components
escapedcat May 24, 2026
bf78a68
refactor(map): drop Next suffix from migrated components
escapedcat May 24, 2026
c6e9a24
refactor(merchant): port /merchant/[id] to MapLibre #1003
escapedcat May 25, 2026
c8d5fdd
refactor(add-location): port map picker to MapLibre #1003
escapedcat May 25, 2026
7cc554c
refactor(communities): port /communities/map to MapLibre #1003
escapedcat May 25, 2026
5efdfc6
chore(deps): remove Leaflet and its plugins #1003
escapedcat May 25, 2026
9f5e508
fix(map): stop shifting CommunityRail left when merchant drawer opens
escapedcat May 25, 2026
a9f5779
fix(map): replace placeholder stubs with real composite pin sprites #…
escapedcat May 25, 2026
eb95a90
chore(css): strip dead Leaflet CSS + stale leaflet comments #1003
escapedcat May 25, 2026
f761531
fix(map): skip geolocate fly arc, ease-in linearly instead #1003
escapedcat May 25, 2026
61961ba
fix(map): spiderfy parent cluster when revealing a selected merchant …
escapedcat May 25, 2026
8cf7e5f
fix(map): iteratively zoom to expansion when revealing a clustered me…
escapedcat May 25, 2026
85e6b45
Revert "fix(map): iteratively zoom to expansion when revealing a clus…
escapedcat May 25, 2026
6b65407
Revert "fix(map): spiderfy parent cluster when revealing a selected m…
escapedcat May 25, 2026
c78fda6
fix(map): zoom search results to 19 so the pin is unclustered #1003
escapedcat May 25, 2026
27879ba
fix(communities-map): force absolute positioning so map fills viewpor…
escapedcat May 25, 2026
352fd1b
fix(area-map): re-sync source + camera on area-to-area navigation #1003
escapedcat May 25, 2026
3206c80
fix(map): preserve user camera on theme swap #1003
escapedcat May 25, 2026
b98a16d
fix(map): reset merchantList + loading state on /map unmount #1003
escapedcat May 25, 2026
a267f33
fix(map): install styleimagemissing placeholder handler on /map #1003
escapedcat May 25, 2026
154c66f
fix(map): restore source-side clustering on AreaMap + MultiPlaceMap #…
escapedcat May 25, 2026
754c4a8
fix(map): route boosted places to a non-clustered source so they esca…
escapedcat May 25, 2026
9d0c6cd
fix(map): bail load handler if onDestroy ran during sprite fetch #1003
escapedcat May 25, 2026
c5b36ab
fix(map): catch hover-hull getClusterLeaves rejection #1003
escapedcat May 25, 2026
185d81b
fix(map): re-localize custom IControls on locale change #1003
escapedcat May 25, 2026
4cbbc2f
fix(communities-map): destroy Socials Svelte instance on popup close …
escapedcat May 25, 2026
7f6bb0e
fix(communities-map): drop noop locale sub registered after await #1003
escapedcat May 25, 2026
db7e29e
test: migrate Playwright suite from Leaflet to MapLibre selectors #1003
escapedcat May 25, 2026
92dfa10
Merge remote-tracking branch 'origin/main' into feat/maplibre-native
escapedcat May 25, 2026
5b55b16
fix(map): swallow spiderfier teardown errors on /map unmount #1003
escapedcat May 25, 2026
8101329
debug(map): swallow handleHashChange throws to keep nav alive #1003
escapedcat May 25, 2026
b9a575b
fix(community-rail): drop map cleanup in onDestroy β€” runs after map.r…
escapedcat May 25, 2026
94acf17
test: align helpers.ts comment with actual __mapPlacesCount hook #1003
escapedcat May 25, 2026
b879685
fix(map): log sprite-fetch failures instead of swallowing them silent…
escapedcat May 25, 2026
3207985
fix(communities-map): build community popup via DOM APIs, not innerHT…
escapedcat May 25, 2026
36f8683
chore(deps): regenerate pnpm-lock.yaml against main to drop unrelated…
escapedcat May 25, 2026
acc93d2
fix(area-map): close drawer when selected merchant leaves new area #1003
escapedcat May 25, 2026
6cc0934
fix(map): normalize longitude delta in haversine for antimeridian #1003
escapedcat May 25, 2026
00ff305
fix(communities-map): emit a Feature per geometry in multipart commun…
escapedcat May 25, 2026
9a6b632
fix(map): render badges + labels on boosted pins too #1003
escapedcat May 25, 2026
66fdd7a
fix(controls): move custom IControl CSS to a shared stylesheet #1003
escapedcat May 25, 2026
b502669
test(communities-map): canvas renders + ?community= deep-link param s…
escapedcat May 25, 2026
081186b
test(community-area): cover area-to-area navigation
escapedcat May 25, 2026
a16cbd9
fix(map): render pin sprites at 3Γ— for phone-class retina sharpness
escapedcat May 26, 2026
c86b596
fix(map): render comment-badge sprite at 2Γ— for retina sharpness
escapedcat May 26, 2026
e11a6f8
fix(map): render saved-badge sprite at 2Γ— for retina sharpness
escapedcat May 26, 2026
f9df4f0
feat(map): restore ?lat=&long= viewport query for legacy embeds #1003
escapedcat May 27, 2026
28d47a0
feat(map): raise maxZoom from 19 to 21 to match legacy Leaflet config…
escapedcat May 27, 2026
c1735a8
feat(map): show a WebGL-unsupported fallback instead of a blank map #…
escapedcat May 27, 2026
0c7db19
feat(map): extend maxZoom 21 to the remaining MapLibre instantiation …
escapedcat May 27, 2026
c92dcdd
feat(map): disable world copies so the map doesn't wrap horizontally …
escapedcat May 27, 2026
8e8855a
fix(map): surface places-sync failures as a toast #1003
escapedcat May 27, 2026
13870a3
fix(map): zoom single search-result hit to 17 instead of 15 #1003
escapedcat May 27, 2026
a871456
feat(map): re-add the bottom-left scale bar #1003
escapedcat May 27, 2026
d66aca4
feat(map): land first-time visitors near their IP-derived location #1003
escapedcat May 27, 2026
1c510b4
feat(map): re-add Support BTC Map link to the attribution #1003
escapedcat May 27, 2026
95435a9
fix(i18n): add missing mapControls.basemapTitle key #1003
escapedcat May 27, 2026
0234526
feat(map): toast on malformed ?lat=&long= URL params #1003
escapedcat May 27, 2026
124baee
fix(map): refresh the pin source on boost / comment update #1003
escapedcat May 27, 2026
0a41ddb
feat(map): filter map pins to search results when in search mode #1003
escapedcat May 27, 2026
01d37da
chore(i18n): remove orphaned map i18n keys #1003
escapedcat May 27, 2026
0e6c9e9
feat(map): instrument zoom-out, locate, and layer-change with analyti…
escapedcat May 27, 2026
0a3f811
feat(map): filter map pins by the panel's category chip #1003
escapedcat May 27, 2026
9427e9f
feat(map): remember the last viewport across sessions #1003
escapedcat May 27, 2026
d18f544
fix(map): give MapLibre controls a dark-mode treatment #1003
escapedcat May 27, 2026
ea33ca5
fix(communities-map): polish the popup styling and add dark-mode supp…
escapedcat May 27, 2026
b421a0b
chore(analytics): drop unused event names #1003
escapedcat May 27, 2026
8871de3
fix(map): repair the popup tip arrow + invert the basemap-picker icon…
escapedcat May 27, 2026
338e8e4
fix(communities-map): drop the dark popup border so it doesn't cut ac…
escapedcat May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@
},
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@maplibre/maplibre-gl-leaflet": "^0.1.3",
"@nazka/map-gl-js-spiderfy": "^2.0.0",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/svelte-table": "^8.21.3",
"@turf/convex": "^7.3.5",
"@turf/helpers": "^7.3.5",
"@zerodevx/svelte-toast": "^0.9.6",
"axios": "^1.15.0",
"axios-retry": "^4.5.0",
Expand All @@ -53,12 +55,8 @@
"husky": "^9.1.7",
"i18n-iso-countries": "^7.14.0",
"js-confetti": "^0.13.1",
"leaflet": "^1.9.4",
"leaflet.featuregroup.subgroup": "^1.0.2",
"leaflet.locatecontrol": "^0.90.0",
"leaflet.markercluster": "^1.5.3",
"localforage": "^1.10.0",
"maplibre-gl": "^4.7.1",
"maplibre-gl": "^5.24.0",
"marked": "^18.0.0",
"opening_hours": "^3.12.0",
"qrcode": "^1.5.4",
Expand All @@ -80,9 +78,6 @@
"@types/d3-geo": "^3.1.0",
"@types/dompurify": "^3.2.0",
"@types/geojson": "^7946.0.16",
"@types/leaflet": "1.9.21",
"@types/leaflet.featuregroup.subgroup": "^1.0.4",
"@types/leaflet.markercluster": "^1.5.6",
"@types/node": "^22.19.18",
"@types/qrcode": "^1.5.6",
"jsdom": "^29.0.2",
Expand Down
368 changes: 147 additions & 221 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

144 changes: 0 additions & 144 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -170,153 +170,9 @@
filter: drop-shadow(0 0 6px #f7931a) drop-shadow(0 0 12px #f7931a);
}
}

/* Fix for Leaflet controls not visible on mobile Firefox/Brave */
/* Only apply to fullscreen map to avoid breaking embedded maps */
.map-fullscreen .leaflet-bottom {
position: fixed !important;
bottom: 0 !important;
}
}

/* Center icons in Leaflet control buttons */
.leaflet-bar a {
display: flex;
align-items: center;
justify-content: center;
}

/* Invert custom button icons in dark mode */
.dark .leaflet-bar a img {
filter: invert(1);
}

/* Dark mode for Leaflet controls - Tailwind gray palette */
:root {
/* Marker label colors - Light mode (Tailwind palette references) */
--marker-label-color: #0e7490; /* cyan-700 - better contrast on varied map backgrounds */
--marker-label-shadow: #ffffff; /* white */
--marker-label-boosted-color: #f97316; /* orange-500 - warmer, more prominent */
--marker-label-boosted-shadow: #ffffff; /* white */
}

.dark {
--leaflet-bg: #1f2937; /* gray-800 */
--leaflet-bg-transparent: rgba(31, 41, 55, 0.8);
--leaflet-border: #374151; /* gray-700 */
--leaflet-border-muted: #6b7280; /* gray-500 */
--leaflet-text: #f3f4f6; /* gray-100 */
--leaflet-text-muted: #9ca3af; /* gray-400 */
--leaflet-link: #60a5fa; /* blue-400 */
--marker-label-color: #22d3ee; /* cyan-400 - bright for dark backgrounds */
--marker-label-shadow: rgba(0, 0, 0, 0.95); /* near-black */
--marker-label-boosted-color: #fb923c; /* orange-400 - bright warm tone */
--marker-label-boosted-shadow: rgba(0, 0, 0, 0.95); /* near-black */
}

.dark .leaflet-bar,
.dark .leaflet-control-layers {
background-color: var(--leaflet-bg);
border-color: var(--leaflet-border);
}

.dark .leaflet-bar a {
color: var(--leaflet-text);
background-color: var(--leaflet-bg);
border-bottom-color: var(--leaflet-border);
}

.dark .leaflet-bar a:hover {
background-color: var(--leaflet-border);
}

.dark .leaflet-control-layers-toggle {
background-color: var(--leaflet-bg);
}

.dark .leaflet-control-layers-list {
background-color: var(--leaflet-bg);
color: var(--leaflet-text);
}

.dark .leaflet-control-attribution {
background-color: var(--leaflet-bg-transparent);
color: var(--leaflet-text-muted);
}

.dark .leaflet-control-attribution a {
color: var(--leaflet-link);
}

.dark .leaflet-control-scale-line {
background-color: var(--leaflet-bg-transparent);
color: var(--leaflet-text);
border-color: var(--leaflet-border-muted);
text-shadow: none;
}

/* Dark mode for select dropdown options (Windows/Linux native rendering fix) */
.dark select option {
background-color: rgb(55 65 81); /* gray-700 */
}

/* Marker label styles for Leaflet permanent tooltips */
.leaflet-tooltip.marker-label,
.leaflet-tooltip.marker-label-boosted {
background: none !important;
border: none !important;
outline: none !important;
box-shadow: none !important;
font-size: 14px;
padding: 2px 4px;
margin: 0;
border-radius: 0;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
max-width: 160px;
min-width: unset;
width: auto;
height: auto;
font-weight: 500;
line-height: 1.2;
}

.leaflet-tooltip.marker-label {
color: var(--marker-label-color);
text-shadow:
-1px -1px 0 var(--marker-label-shadow),
1px -1px 0 var(--marker-label-shadow),
-1px 1px 0 var(--marker-label-shadow),
1px 1px 0 var(--marker-label-shadow);
}

.leaflet-tooltip.marker-label-boosted {
color: var(--marker-label-boosted-color);
font-weight: 600;
text-shadow:
-1px -1px 0 var(--marker-label-boosted-shadow),
1px -1px 0 var(--marker-label-boosted-shadow),
-1px 1px 0 var(--marker-label-boosted-shadow),
1px 1px 0 var(--marker-label-boosted-shadow);
filter: drop-shadow(0 1px 2px rgba(249, 115, 22, 0.35));
}

/* Z-index layering for marker labels and tooltips
* - Tooltips (350): Below markers to prevent interaction blocking
* - Markers: Use Leaflet's default z-index management for proper interaction states
* - Clusters: Default Leaflet z-index (higher, not modified)
*/
.leaflet-map-pane .leaflet-tooltip-pane {
z-index: 350 !important;
}

/* Hide the tooltip arrow/pointer for permanent labels */
.leaflet-tooltip.marker-label::before,
.leaflet-tooltip.marker-label .leaflet-tooltip-tip-container,
.leaflet-tooltip.marker-label-boosted::before,
.leaflet-tooltip.marker-label-boosted .leaflet-tooltip-tip-container {
display: none !important;
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
<script lang="ts">
import rewind from "@mapbox/geojson-rewind";
import type { GeoJSON as LeafletGeoJSON, Map as LeafletMap } from "leaflet";
import { onDestroy, onMount } from "svelte";
import type { Feature, FeatureCollection } from "geojson";
import type { GeoJSONSource, Map as MapLibreMap } from "maplibre-gl";
import { onMount } from "svelte";
import { fly } from "svelte/transition";

import {
MAP_PANEL_MARGIN,
MERCHANT_DRAWER_WIDTH,
PANEL_DRAWER_GAP,
} from "$lib/constants";
import { MAP_PANEL_MARGIN } from "$lib/constants";
import { merchantDrawer } from "$lib/merchantDrawerStore";
import { merchantList } from "$lib/merchantListStore";
import { areas } from "$lib/store";
import { areasSync } from "$lib/sync/areas";
import type { Area, Leaflet } from "$lib/types";
import type { Area } from "$lib/types";
import { areaIconSrc, getCommunitiesAtCoordinates } from "$lib/utils";

import { resolve } from "$app/paths";

// Enter from 10px below (slides up), exit reverses. Y motion avoids the
// horizontal scrollbar that X motion caused when the rail sat at the viewport edge.
// Community rail rendered alongside /map. Preview polygon lives on a
// dedicated GeoJSON source + fill/outline layers added once and reused
// β€” re-using the layer beats re-adding it per hover when the user
// rapid-fires the avatars.

const DESKTOP_FLY = { duration: 280, y: 10 };
// Mobile rail is anchored top-left, so enter from above sliding down.
const MOBILE_FLY = { duration: 280, y: -10 };

const SOURCE_ID = "community-preview";
const FILL_LAYER_ID = "community-preview-fill";
const OUTLINE_LAYER_ID = "community-preview-outline";

const EMPTY_FC: FeatureCollection = { type: "FeatureCollection", features: [] };

export let lat: number | null = null;
export let lon: number | null = null;
export let zoom: number | null = null;
export let map: LeafletMap | undefined = undefined;
export let leaflet: Leaflet | undefined = undefined;
export let map: MapLibreMap | undefined = undefined;

const MIN_ZOOM = 6;
const MOBILE_VISIBLE_LIMIT = 4;
Expand All @@ -41,13 +45,11 @@ $: allCommunities =
$: mobileVisible = allCommunities.slice(0, MOBILE_VISIBLE_LIMIT);
$: mobileOverflow = Math.max(0, allCommunities.length - MOBILE_VISIBLE_LIMIT);

$: rightOffset = $merchantDrawer.isOpen
? MAP_PANEL_MARGIN + MERCHANT_DRAWER_WIDTH + PANEL_DRAWER_GAP
: MAP_PANEL_MARGIN;
let previewCommunityId: string | null = null;
let layersAdded = false;

// Clear hover preview if the previewed community is no longer visible.
// This handles the case where the rail DOM is removed (pan / zoom-out)
// before mouseleave or blur fires on the hovered anchor.
// Clear hover preview if the previewed community is no longer visible
// (rail removed by pan / zoom-out before mouseleave/blur fires).
$: if (
previewCommunityId &&
!allCommunities.some((c) => c.id === previewCommunityId)
Expand All @@ -66,53 +68,83 @@ const handleImgError = (e: Event) => {
img.src = "/images/bitcoin.svg";
};

let previewLayer: LeafletGeoJSON | null = null;
let previewCommunityId: string | null = null;
const ensureLayers = (m: MapLibreMap) => {
if (layersAdded) return;
if (!m.getSource(SOURCE_ID)) {
m.addSource(SOURCE_ID, { type: "geojson", data: EMPTY_FC });
}
if (!m.getLayer(FILL_LAYER_ID)) {
m.addLayer({
id: FILL_LAYER_ID,
type: "fill",
source: SOURCE_ID,
paint: {
"fill-color": "#F7931A",
"fill-opacity": 0.3,
},
});
}
if (!m.getLayer(OUTLINE_LAYER_ID)) {
m.addLayer({
id: OUTLINE_LAYER_ID,
type: "line",
source: SOURCE_ID,
paint: {
"line-color": "#000000",
"line-width": 1,
},
});
}
layersAdded = true;
};

function clearPreview() {
if (previewLayer && map) {
map.removeLayer(previewLayer);
}
previewLayer = null;
previewCommunityId = null;
if (!map) return;
const source = map.getSource(SOURCE_ID) as GeoJSONSource | undefined;
source?.setData(EMPTY_FC);
}

function showPreview(community: Area) {
if (!map || !leaflet || !community.tags.geo_json) return;
clearPreview();
if (!map || !community.tags.geo_json) return;
ensureLayers(map);
const source = map.getSource(SOURCE_ID) as GeoJSONSource | undefined;
if (!source) return;
try {
const gj = rewind(community.tags.geo_json, true);
previewLayer = leaflet
.geoJSON(gj, {
style: {
color: "#000000",
weight: 1,
fillColor: "#F7931A",
fillOpacity: 0.3,
},
interactive: false,
})
.addTo(map);
// Right-hand-rule winding for fill rendering correctness.
const gj = rewind(community.tags.geo_json, true) as
| Feature
| FeatureCollection;
source.setData(gj);
previewCommunityId = community.id;
} catch (e) {
console.error("CommunityRail: failed to draw preview", e);
}
}

onMount(() => {
// /map does not otherwise sync the areas store; trigger it here so the
// rail has data. areasSync has its own 5-min cache so repeat calls are cheap.
// /map doesn't otherwise sync the areas store; trigger it here so the
// rail has data. areasSync has its own 5-min cache.
areasSync();
});

onDestroy(clearPreview);
// No onDestroy map cleanup: /map (the owner) calls map.remove() in its
// own onDestroy, which runs before this child component's fragment is
// torn down. By that point the MapLibre Map's internal `style` is
// undefined, so any m.getLayer / m.getSource here threw
// Cannot read properties of undefined (reading 'getLayer')
// even though `if (!map) return` passed β€” the JS object still exists,
// the bare-method `m.getLayer` just deferences a now-undefined `m.style`.
// map.remove() already disposes our sources and layers.
</script>

{#if allCommunities.length > 0}
<!-- Desktop: bottom-right, above Leaflet attribution. Shifts left when drawer opens. -->
<!-- Desktop: bottom-right, above the attribution. The merchant drawer + list
panel live on the left side of the map, so the rail's right offset is
fixed. -->
<div
class="pointer-events-none absolute bottom-10 z-[1001] hidden flex-col-reverse gap-2 transition-[right] duration-200 md:flex"
style="right: {rightOffset}px"
class="pointer-events-none absolute bottom-10 z-[1001] hidden flex-col-reverse gap-2 md:flex"
style="right: {MAP_PANEL_MARGIN}px"
>
{#each allCommunities as community (community.id)}
<a
Expand All @@ -137,7 +169,7 @@ onDestroy(clearPreview);
{/each}
</div>

<!-- Mobile: top-left. Hidden when the merchant drawer or the nearby/worldwide list is open so the active merchant context owns the screen. -->
<!-- Mobile: top-left. Hidden when drawer or list panel is open. -->
<div
class="pointer-events-none absolute top-3 left-3 z-[1001] flex flex-col gap-1.5 md:hidden"
class:hidden={$merchantDrawer.isOpen || $merchantList.isOpen}
Expand Down
Loading
Loading