From e8ad97c726f3c01ad8ae482e4f7b3b68afbe35cd Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 25 Apr 2026 21:07:53 -0700 Subject: [PATCH 1/2] fix(hillshade): apply mix-blend-mode per tile image to fix dropouts at max zoom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The blend was applied to .hillshade-blend (the Leaflet layer's outer container). That element shares the tile pane's stacking context with the base layer below it, but its inner .leaflet-tile-container uses translate3d for hardware-accelerated zoom animation, which can intermittently establish its own stacking context that isolates a container-level blend from siblings — visible as the multiply blend "sometimes not applying" at the closest zoom levels (most noticeable on the topo basemap, where Esri's hillshade upscales from z=16). Targeting img.leaflet-tile under .hillshade-blend keeps the blend on the leaf elements that render directly under the transform, avoiding the isolation. Each tile image blends individually with the base map. Closes #168 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/style.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/style.css b/src/style.css index f2c7029..3a56afe 100644 --- a/src/style.css +++ b/src/style.css @@ -9,7 +9,10 @@ body { height: 100%; width: 100%; padding: 0; margin: 0; } #map { position: absolute; top: 0; bottom: 0; right: 0; left: 0; } /* Multiply preserves base-map color in flat areas (hillshade ≈ white there) and only darkens slopes. */ -.hillshade-blend { mix-blend-mode: multiply; } +/* Applied at the tile-image level — the layer's outer container shares a stacking context with the */ +/* base layer, but its inner .leaflet-tile-container uses translate3d that can intermittently isolate */ +/* a container-level blend from siblings during zoom animations (most visible at max zoom). */ +.hillshade-blend img.leaflet-tile { mix-blend-mode: multiply; } /* Icon state classes for GPS accuracy filter visual feedback */ #disabled { From 7d3cbcd30a97c5e3aa5c624589ad4b841906f05c Mon Sep 17 00:00:00 2001 From: Test Date: Sat, 25 Apr 2026 21:10:34 -0700 Subject: [PATCH 2/2] fix: address PR #169 review - Trim hillshade comment to one line per project convention. - Refresh CLAUDE.md: vitest is now in use (628 tests), recording is replaced by guidance, file-structure entry expanded with the new guidance / routing / geo modules, and the test-claim staleness flagged by the reviewer is resolved. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 31 +++++++++++++++++++++---------- src/style.css | 5 +---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dc376df..f49157a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,15 +6,17 @@ Project-specific guidance for webmap.dev. Global workflow preferences are in `~/ **webmap.dev** is a GPS mapping web app built with Leaflet and ESRI geocoding. Key features: -- Real-time GPS location tracking with centering and breadcrumb trail recording +- Real-time GPS location tracking with three-state locate (off / active-following / passive) +- Turn-by-turn routed navigation via FOSSGIS Valhalla (driving / cycling / walking) - Reverse geocoding (pin drop → address lookup) with clipboard copy - Address search via ESRI Leaflet Geocoder +- Offline tile pre-download + Workbox passive caching (PWA) ## Tech Stack - **Vite** + **TypeScript** (ES2020 target, strict mode, `noUncheckedIndexedAccess`) - **Leaflet** 1.9 + **esri-leaflet** 3 + **esri-leaflet-geocoder** 3 -- No test framework (no `test` script in package.json) +- **vitest** for unit tests (pure-function tests; no DOM/Leaflet integration) ## Key Commands @@ -24,6 +26,8 @@ npm run build # Production build npm run preview # Preview production build npm run type-check # TypeScript check (tsc --noEmit) npm run lint # ESLint +npm test # vitest run +npm run size # size-limit (gzipped JS bundle, capped at 100 kB) ``` ## File Structure @@ -32,21 +36,28 @@ npm run lint # ESLint src/ ├── main.ts # Entry point — wires all modules together ├── types.ts # AppState interface + createInitialState() - ├── map.ts # Leaflet map initialization - ├── controls.ts # UI controls (clipboard, centering, tracking toggles) - ├── geocoding.ts # Address search + reverse geocoding - ├── location.ts # GPS locationfound event handler - ├── timer.ts # GPS polling loop (scheduleUpdateCallback) + ├── map.ts # Leaflet map + tile-layer config + ├── layers-control.ts # Base-map / overlay popover control + ├── controls.ts # Bottom-left thumb cluster (locate, version, attribution) + ├── geocoding.ts # Address search + reverse geocoding + Navigate-here + ├── location.ts # GPS locationfound handler + heading-cone wedge + ├── guidance.ts # Turn-by-turn navigation state machine + pill UI + ├── routing.ts # Valhalla client + polyline6 decoder + ├── geo.ts # haversineDistance, bearingDeg, pointToSegmentMeters + ├── timer.ts # GPS polling refcount loop + ├── consent.ts # First-run privacy consent modal + ├── keepalive.ts # Wake Lock + silent-audio loop for background GPS └── style.css # App styles +docs/adr/ # Architecture Decision Records ``` ## Architecture Pattern -**Single AppState object** (`types.ts`) threaded by reference through all modules — no event bus or state management library. Modules mutate state directly. +**Single AppState object** (`types.ts`) threaded by reference through all modules — no event bus or state management library. Modules mutate state directly. See [ADR-001](docs/adr/ADR-001-single-mutable-state.md). -**GPS polling uses a refcount** (`updateCallback: number`) so centering and tracking can independently request/release the polling loop without stepping on each other (0 = stopped, 1 = centering only, 2 = centering + tracking). +**GPS polling uses a refcount** (`updateCallback: number`) so locate, guidance, and other consumers can independently request/release the watch without stepping on each other. See [ADR-002](docs/adr/ADR-002-refcount-gps-polling.md). ## Notes - `esri-leaflet-geocoder.d.ts` is a local type stub — the package lacks complete TypeScript types -- No automated tests — verify changes with `npm run type-check && npm run lint` and manual browser testing +- Verify changes with `npm run type-check && npm run lint && npm test`; UI changes also require manual browser testing diff --git a/src/style.css b/src/style.css index 3a56afe..0ef08ba 100644 --- a/src/style.css +++ b/src/style.css @@ -8,10 +8,7 @@ html { height: 100%; width: 100%; padding: 0; margin: 0; } body { height: 100%; width: 100%; padding: 0; margin: 0; } #map { position: absolute; top: 0; bottom: 0; right: 0; left: 0; } -/* Multiply preserves base-map color in flat areas (hillshade ≈ white there) and only darkens slopes. */ -/* Applied at the tile-image level — the layer's outer container shares a stacking context with the */ -/* base layer, but its inner .leaflet-tile-container uses translate3d that can intermittently isolate */ -/* a container-level blend from siblings during zoom animations (most visible at max zoom). */ +/* Per-tile multiply — a container-level blend can isolate under .leaflet-tile-container's translate3d. */ .hillshade-blend img.leaflet-tile { mix-blend-mode: multiply; } /* Icon state classes for GPS accuracy filter visual feedback */