From 4ea1bf8ebc24a3097ab6ecf781feaee69c747577 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Sat, 16 May 2026 13:01:00 -0700 Subject: [PATCH] =?UTF-8?q?fix(#1236):=20map=20mobile=20=E2=80=94=20sticky?= =?UTF-8?q?=20panel=20header=20+=20remove=20right=20gutter=20(#1237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RED: 862d7c82 — E2E asserting (A) leaflet-map width == viewport on mobile and (B) sticky panel header. CI URL: see Checks tab. Fixes #1236. ## Sub-issue A — Map Controls panel scroll affordance **Root cause:** `.map-controls` already had `max-height` + `overflow-y: auto`, but the `

` title was static — once the panel scrolled, the title scrolled away with it and users lost the affordance that they were inside a scroll container. No visual cue, no anchor. **Fix:** make `.map-controls h3` `position: sticky` at the top of the scroll container (pulled flush to the panel edges with negative margins so it covers the corner radius cleanly), with the panel `--card-bg` background and a `--border` bottom rule. Added `scrollbar-gutter: stable` so the scroll indicator is consistently present. ## Sub-issue B — Map canvas offset left with right gutter **Root cause:** `.map-side-pane` (Path Inspector) is `flex: 0 0 32px` inside the flexbox `#map-wrap`. At every viewport width that 32px is consumed before the leaflet canvas gets sized, leaving an unused band on the right. Desktop has room for it; mobile (375px viewport) does not — and Path Inspector hex-prefix entry is impractical on a phone anyway. **Fix:** `display: none` on `.map-side-pane` at `≤640px`. Leaflet canvas now fills 100% of the viewport. ## Verification - E2E `test-issue-1236-map-mobile-e2e.js` covers both at 375x800 + desktop guard at 1280x800. RED commit (`862d7c82`) failed 2/3 mobile assertions; GREEN commit (`85efcba7`) passes 3/3. - Map canvas width at 375x800: **343px → 375px**. - Existing channels mobile E2E (#1224) still passes. - Desktop (1280px): panel stays `position: absolute`, Path Inspector pane still present. All colors via CSS variables. No JS changes. --------- Co-authored-by: OpenClaw Bot --- .github/workflows/deploy.yml | 1 + public/style.css | 25 +++++- test-issue-1236-map-mobile-e2e.js | 123 ++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 test-issue-1236-map-mobile-e2e.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 231bc2d5..8d1d3e55 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -259,6 +259,7 @@ jobs: CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1204-live-panel-structure-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-vcr-overlap-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt + BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-resize-observer-leak-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-nav-drawer-1064-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/public/style.css b/public/style.css index a68544a2..0cfd7a72 100644 --- a/public/style.css +++ b/public/style.css @@ -1068,8 +1068,24 @@ body.scroll-locked { overflow: hidden; } max-width: calc(100vw - 24px); font-size: 13px; max-height: calc(100% - 24px); overflow-y: auto; + /* #1236: keep the scrollbar gutter reserved so the scroll affordance is + visible even when content briefly fits — users get a consistent visual + cue that the panel is scrollable. */ + scrollbar-gutter: stable; +} +/* #1236: sticky panel title so the scroll affordance is always visible at the + top of the scroll container when content overflows on small viewports. */ +.map-controls h3 { + font-size: 15px; + margin: -14px -16px 10px -16px; + padding: 10px 16px; + position: sticky; + top: -14px; + background: var(--card-bg); + z-index: 2; + border-bottom: 1px solid var(--border); + border-radius: 10px 10px 0 0; } -.map-controls h3 { font-size: 15px; margin-bottom: 10px; } .mc-section { margin-bottom: 10px; } .mc-label { font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .3px; color: var(--text-muted); margin-bottom: 4px; } .mc-section label { display: block; padding: 2px 0; cursor: pointer; } @@ -3203,6 +3219,13 @@ th.sort-active { color: var(--accent, #60a5fa); } .map-side-pane .pane-toggle { cursor: pointer; padding: 8px; font-size: 14px; text-align: center; } .map-side-pane .pane-content { display: none; } .map-side-pane.expanded .pane-content { display: block; } +/* #1236: on mobile the path-inspector side pane was eating 32px of horizontal + width (flex:0 0 32px) inside #map-wrap, leaving an unused gutter on the + right of the leaflet canvas. Path Inspector is a desktop-only convenience; + hide the pane on narrow viewports so the map fills 100% of the viewport. */ +@media (max-width: 640px) { + .map-side-pane { display: none; } +} /* Tools landing page */ .tools-landing { padding: 24px; max-width: 600px; } diff --git a/test-issue-1236-map-mobile-e2e.js b/test-issue-1236-map-mobile-e2e.js new file mode 100644 index 00000000..05b2efe9 --- /dev/null +++ b/test-issue-1236-map-mobile-e2e.js @@ -0,0 +1,123 @@ +/** + * E2E (#1236): Map page mobile layout. + * + * At 375x800 viewport the /#/map page must: + * - Collapsed map controls: leaflet map canvas width must equal the viewport + * width (within 1px tolerance). No silent gutter on the right. + * - Expanded map controls: the panel must either fit within viewport OR + * have a sticky element at the top of the scroll container AND + * `overflow-y: auto` so the scroll affordance is real. + * + * Desktop guard (≥768px): map controls panel layout must remain absolute + * (position: absolute), not stretched full width. + * + * Run: BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js + */ +'use strict'; +const { chromium } = require('playwright'); + +const BASE = process.env.BASE_URL || 'http://localhost:13581'; + +let passed = 0, failed = 0; +async function step(name, fn) { + try { await fn(); passed++; console.log(' \u2713 ' + name); } + catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); } +} +function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } + +async function run() { + const launchOpts = { args: ['--no-sandbox'] }; + if (process.env.CHROMIUM_PATH) launchOpts.executablePath = process.env.CHROMIUM_PATH; + const browser = await chromium.launch(launchOpts); + + // === Mobile: 375x800 === + const ctx = await browser.newContext({ viewport: { width: 375, height: 800 } }); + const page = await ctx.newPage(); + + await page.goto(BASE + '/#/map', { waitUntil: 'load', timeout: 60000 }); + await page.waitForSelector('#leaflet-map', { timeout: 10000 }); + await page.waitForSelector('#mapControls', { state: 'attached', timeout: 10000 }); + await page.waitForTimeout(500); + + await step('mobile collapsed: leaflet-map width fills viewport (no right gutter)', async () => { + // Ensure controls panel is collapsed (default on mobile per map.js) + const data = await page.evaluate(() => { + const panel = document.getElementById('mapControls'); + const btn = document.getElementById('mapControlsToggle'); + if (panel && !panel.classList.contains('collapsed')) btn && btn.click(); + const lm = document.getElementById('leaflet-map'); + return { + mapW: lm ? Math.round(lm.getBoundingClientRect().width) : null, + mapLeft: lm ? Math.round(lm.getBoundingClientRect().left) : null, + vw: window.innerWidth, + }; + }); + assert(data.mapW !== null, 'leaflet-map not found'); + // Map must start at left edge and span to right edge (allow 1px rounding). + assert(data.mapLeft <= 1, + 'leaflet-map must start at viewport left, got left=' + data.mapLeft); + assert(data.mapW >= data.vw - 1, + 'leaflet-map width must equal viewport (' + data.vw + 'px), got ' + data.mapW + 'px'); + }); + + await step('mobile expanded: panel has sticky header AND overflow-y auto', async () => { + const data = await page.evaluate(() => { + const panel = document.getElementById('mapControls'); + const btn = document.getElementById('mapControlsToggle'); + if (panel.classList.contains('collapsed')) btn && btn.click(); + const cs = getComputedStyle(panel); + const h3 = panel.querySelector('h3'); + const hcs = h3 ? getComputedStyle(h3) : null; + return { + overflowY: cs.overflowY, + h3Position: hcs ? hcs.position : null, + scrollGutter: cs.scrollbarGutter || '', + scrollH: panel.scrollHeight, + clientH: panel.clientHeight, + isScrollable: panel.scrollHeight > panel.clientHeight + 1, + }; + }); + // Require explicit sticky header + scrollable overflow regardless of + // whether content currently overflows (so future content additions are + // covered too). + assert(data.h3Position === 'sticky', + 'panel h3 must be position:sticky (scroll affordance), got ' + data.h3Position); + assert(data.overflowY === 'auto' || data.overflowY === 'scroll', + 'panel overflow-y must be auto/scroll, got ' + data.overflowY); + }); + + await ctx.close(); + + // === Desktop: 1280x800 — guard against regression === + const ctx2 = await browser.newContext({ viewport: { width: 1280, height: 800 } }); + const p2 = await ctx2.newPage(); + await p2.goto(BASE + '/#/map', { waitUntil: 'load', timeout: 60000 }); + await p2.waitForSelector('#mapControls', { state: 'attached', timeout: 10000 }); + await p2.waitForTimeout(300); + + await step('desktop (1280px): map controls panel position is absolute', async () => { + const data = await p2.evaluate(() => { + const panel = document.getElementById('mapControls'); + const cs = getComputedStyle(panel); + const rect = panel.getBoundingClientRect(); + return { + position: cs.position, + width: Math.round(rect.width), + vw: window.innerWidth, + }; + }); + assert(data.position === 'absolute', + 'desktop panel must be position:absolute, got ' + data.position); + // Should be modest width (not full viewport) + assert(data.width < data.vw * 0.5, + 'desktop panel must be <50% viewport width, got ' + data.width + '/' + data.vw); + }); + + await browser.close(); + + console.log('\n' + passed + '/' + (passed + failed) + ' tests passed' + + (failed ? ', ' + failed + ' failed' : '')); + process.exit(failed > 0 ? 1 : 0); +} + +run().catch(err => { console.error('Fatal:', err); process.exit(1); });