From 4f7bb245d4d9a807aeaedeeb379f20f5efaacae0 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Wed, 8 Jul 2026 18:14:01 -0700 Subject: [PATCH] fix(#1833): pin legend toggle button above VCR bar on Live view (#1834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #1833 — the `.legend-toggle-btn` (palette icon) on the Live view was hardcoded to `bottom: 1rem`, so on typical desktop viewports it sat underneath the VCR playback bar and was unreachable. The reporter had to hide the legend via `localStorage` to work around it. ## Fix `public/live.css:1313` — one-character-class change: ```diff .legend-toggle-btn { position: fixed; - bottom: 1rem; + bottom: calc(var(--vcr-bar-height, 58px) + 10px); right: 1rem; ``` Mirrors the existing pattern already used by every other bottom-pinned Live overlay: - `.live-feed` (live.css:1204) - `.live-overlay[data-position="br"]` (live.css:1372) - `.feed-show-btn` (live.css:903) `--vcr-bar-height` is maintained by the ResizeObserver on `.vcr-bar`, so the button now tracks bar growth (mobile two-row layout, safe-area-inset) instead of overlapping. Same class of regression as #685 / #1206 / #1107 — an overlay that was missed in the prior sweep. ## TDD - Red commit `f6a938b7`: `test-issue-1833-legend-toggle-vcr-offset.js` asserts `.legend-toggle-btn`'s `bottom` declaration references `var(--vcr-bar-height`. Fails on assertion (not build error) against the hardcoded `1rem`. - Green commit `e399092b`: the CSS one-liner above. All 3 assertions pass. Grep-based CSS assertion per AGENTS.md § "E2E-DOM-grep exemption" — there is no existing Playwright test in this area that measures the toggle-button rect against `--vcr-bar-height`, and standing one up would be disproportionate for a single-property fix that mirrors three existing, tested overlay patterns. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all hard gates pass, no warnings. ## Browser verified Not required per fix-issue skill (CSS-only, mirrors three existing tested patterns). Staging will pick up the change on merge; visual regression will be caught if the mirrored pattern breaks (grep test in this PR + existing E2E tests on the sister overlays). --------- Co-authored-by: corescope-bot Co-authored-by: openclaw-bot --- public/live.css | 2 +- test-issue-1107-live-layout.js | 4 +- test-issue-1833-legend-toggle-vcr-offset.js | 78 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 test-issue-1833-legend-toggle-vcr-offset.js diff --git a/public/live.css b/public/live.css index 92e94121..1943ff58 100644 --- a/public/live.css +++ b/public/live.css @@ -1310,7 +1310,7 @@ input.live-node-filter-input:focus { * inside the map container. */ .legend-toggle-btn { position: fixed; - bottom: 1rem; + bottom: calc(var(--vcr-bar-height, 58px) + 10px); right: 1rem; z-index: 500; background: color-mix(in srgb, var(--surface-1) 92%, transparent); diff --git a/test-issue-1107-live-layout.js b/test-issue-1107-live-layout.js index f42d0bd5..0e5a682a 100644 --- a/test-issue-1107-live-layout.js +++ b/test-issue-1107-live-layout.js @@ -90,8 +90,8 @@ if (legendBtn) { '.legend-toggle-btn uses position: fixed (pinned to viewport)' ); assert( - /bottom\s*:\s*1rem/.test(legendBtn), - '.legend-toggle-btn pinned at bottom: 1rem' + /bottom\s*:\s*(1rem|max\(1rem|calc\(\s*var\(--vcr-bar-height)/.test(legendBtn), + '.legend-toggle-btn pinned at bottom: 1rem / max(1rem,...) / calc(var(--vcr-bar-height,...)+...) (VCR-bar-aware, #1833)' ); assert( /right\s*:\s*1rem/.test(legendBtn), diff --git a/test-issue-1833-legend-toggle-vcr-offset.js b/test-issue-1833-legend-toggle-vcr-offset.js new file mode 100644 index 00000000..00d99b5b --- /dev/null +++ b/test-issue-1833-legend-toggle-vcr-offset.js @@ -0,0 +1,78 @@ +/** + * Regression (#1833): the .legend-toggle-btn on the Live view was hardcoded + * to `bottom: 1rem`, so it sat underneath the VCR playback bar and was + * unreachable. Sister overlays (.live-feed, .live-overlay[data-position="br"], + * .feed-show-btn) all offset their bottom by `var(--vcr-bar-height, ...)`. + * + * Same class of regression as #685 / #1206 / #1107. Test asserts the CSS rule + * uses --vcr-bar-height so bottom-pinned Live overlays stay pinned above the + * VCR bar. Grep-based per AGENTS.md E2E-DOM-grep exemption. + * + * Run: node test-issue-1833-legend-toggle-vcr-offset.js + */ +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const cssPath = path.join(__dirname, 'public', 'live.css'); +const css = fs.readFileSync(cssPath, 'utf8'); + +let passed = 0, failed = 0; +function step(name, fn) { + try { 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'); } + +// Extract a rule body whose selector list is EXACTLY the given selector +// (avoids matching compound rules like `.live-feed .panel-content`). +function extractRule(source, selector) { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp('(?:^|})\\s*' + escaped + '\\s*\\{', 'm'); + const m = re.exec(source); + assert(m, 'selector not found (exact match): ' + selector); + const open = source.indexOf('{', m.index); + const close = source.indexOf('}', open); + assert(open !== -1 && close !== -1, 'malformed rule for: ' + selector); + return source.slice(open + 1, close); +} + +console.log('#1833: .legend-toggle-btn must offset for --vcr-bar-height'); + +step('.legend-toggle-btn rule exists', () => { + assert(/\.legend-toggle-btn\s*\{/.test(css), '.legend-toggle-btn rule missing from live.css'); +}); + +step('.legend-toggle-btn bottom uses var(--vcr-bar-height)', () => { + // There may be several `.legend-toggle-btn { ... }` blocks (media queries, + // fullscreen). Only the one that declares `bottom` matters here. + const re = /\.legend-toggle-btn\s*\{([^}]*)\}/g; + let m, bottomValue = null; + while ((m = re.exec(css))) { + const bodyMatch = m[1].match(/(?:^|;|\s)bottom\s*:\s*([^;]+);/); + if (bodyMatch) { bottomValue = bodyMatch[1].trim(); break; } + } + assert(bottomValue !== null, 'no .legend-toggle-btn rule declares `bottom`'); + assert( + /var\(\s*--vcr-bar-height/.test(bottomValue), + '.legend-toggle-btn bottom does not reference --vcr-bar-height (got: ' + bottomValue + ')' + ); +}); + +step('sister overlays still track --vcr-bar-height (guard against regression sweep)', () => { + // Sanity check: the pattern we mirror is the same one already used by + // .live-feed, .live-overlay[data-position="br"], and .feed-show-btn. + // These selectors each have at least one rule whose body sets + // `bottom: calc(var(--vcr-bar-height, ...) ...)`. + const patterns = [ + /\.live-feed\s*\{[^}]*var\(\s*--vcr-bar-height/, + /\.live-overlay\[data-position="br"\][^{]*\{[^}]*var\(\s*--vcr-bar-height/, + /\.feed-show-btn\s*\{[^}]*var\(\s*--vcr-bar-height/, + ]; + for (const re of patterns) { + assert(re.test(css), 'sister overlay pattern missing: ' + re.source); + } +}); + +console.log('\nResults: ' + passed + ' passed, ' + failed + ' failed'); +if (failed > 0) process.exit(1);