mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-20 16:40:54 +00:00
## Summary Fixes #1273 — `.node-top-row .node-qr-wrap` was 2-3× taller than the QR canvas inside it, leaving empty translucent space below the QR. ## Root cause Three compounding issues: 1. **SVG intrinsic height not constrained.** `qrcode-generator` emits an SVG with fixed `width`/`height` attributes (e.g. 147×147). The CSS rule `.node-qr svg { max-width: 100px }` (and 72px mobile) constrains *width* only, so the svg's intrinsic height (147px) is preserved and the wrap is sized to that. 2. **Flex stretch.** `.node-top-row` is `display:flex` with default `align-items:stretch`, so the QR column was forced to match the map column's height (~280px) on desktop. 3. **Excess padding/margin** added another ~24px above and below the visible QR. ## Fix Three small CSS changes in `public/style.css`: | change | effect | |---|---| | `.node-qr svg { height: auto; }` | svg height scales with constrained width | | `.node-top-row .node-qr-wrap { align-self: flex-start; }` | wrap sizes to content, not column | | `.node-top-row .node-qr-wrap { padding: 8px; }` + zero inner `.node-qr` margin-top | tight hug | ## Measurements (real-data fixture, full node detail page) | viewport | wrap.height before | wrap.height after | QR canvas | |---|---|---|---| | 375×800 (mobile overlay) | 165px | **82px** | 72×72 | | 1280×800 (desktop side-by-side) | 217px | **154px** | 100×100 (+ 28px caption) | Overlay remains `position:absolute` top-right on mobile; the original #1243 behavior is preserved. ## TDD - **RED**: `test-issue-1273-qr-overlay-height-e2e.js` asserts wrap height ≤ visible QR + caption + 32px at 375×800 and 1280×800. Failed on master with deltas of 93px (mobile) and 89px (desktop). - **GREEN**: both viewports pass after the CSS fix. Wired into the deploy workflow alongside the other `test-issue-*-e2e.js` runs. ## Acceptance checklist - [x] Container height ≈ QR canvas height + 16-24px padding total - [x] No empty translucent space below the QR - [x] E2E asserts at 375×800 and 1280×800 - [x] Desktop layout unchanged (overlay position preserved; column no longer stretches but the QR card is the same width) - [x] All colors via CSS variables - [x] #1243 overlay behavior preserved (still top-right on mobile, still rendered) ## Commits - `e9d75c92` test(#1273): RED - `13899270` fix(#1273): collapse QR overlay wrap --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
co-authored by
openclaw-bot
parent
21b6eb0d63
commit
c1d94f7db5
@@ -264,6 +264,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1244-live-vcr-row-hints-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
|
||||
BASE_URL=http://localhost:13581 node test-issue-1273-qr-overlay-height-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
|
||||
|
||||
|
||||
+5
-4
@@ -2170,12 +2170,12 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
|
||||
}
|
||||
.observer-selector { display: flex; gap: 4px; margin-bottom: 12px; flex-wrap: wrap; }
|
||||
.node-qr { text-align: center; margin-top: 8px; }
|
||||
.node-qr svg { max-width: 100px; border-radius: 4px; }
|
||||
.node-qr svg { max-width: 100px; height: auto; border-radius: 4px; }
|
||||
[data-theme="dark"] .node-qr svg rect[fill="#ffffff"] { fill: var(--card-bg); }
|
||||
[data-theme="dark"] .node-qr svg rect[fill="#000000"] { fill: var(--text); }
|
||||
.node-map-qr-wrap { position: relative; }
|
||||
.node-map-qr-overlay { position: absolute; bottom: 8px; right: 8px; z-index: 400; background: rgba(255,255,255,0.5); border-radius: 4px; padding: 4px; line-height: 0; margin: 0; text-align: center; }
|
||||
.node-map-qr-overlay svg { max-width: 56px !important; display: block; margin: 0; }
|
||||
.node-map-qr-overlay svg { max-width: 56px !important; height: auto; display: block; margin: 0; }
|
||||
[data-theme="dark"] .node-map-qr-overlay { background: rgba(255,255,255,0.4); }
|
||||
|
||||
/* Replay on Live Map button in packet detail */
|
||||
@@ -2462,7 +2462,8 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); }
|
||||
.node-top-row { display: flex; gap: 16px; margin-bottom: 12px; }
|
||||
.node-top-row .node-map-wrap { flex: 3; min-height: 200px; border-radius: 8px; overflow: hidden; }
|
||||
.node-top-row .node-map-wrap .node-detail-map { height: 100%; }
|
||||
.node-top-row .node-qr-wrap { flex: 1; min-width: 120px; max-width: 160px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
||||
.node-top-row .node-qr-wrap { flex: 1; align-self: flex-start; min-width: 120px; max-width: 160px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; padding: 8px; }
|
||||
.node-top-row .node-qr-wrap .node-qr { margin-top: 0; }
|
||||
.node-qr-wrap--full { max-width: 240px; margin: 0 auto; }
|
||||
.node-stats-table { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 12px; }
|
||||
.node-stats-table td { padding: 6px 12px; border-bottom: 1px solid var(--border); }
|
||||
@@ -2500,7 +2501,7 @@ tr[data-hops]:hover { background: rgba(59,130,246,0.1); }
|
||||
line-height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.node-top-row .node-qr-wrap .node-qr svg { max-width: 72px; }
|
||||
.node-top-row .node-qr-wrap .node-qr svg { max-width: 72px; height: auto; }
|
||||
/* Hide the redundant pubkey caption inside the overlay QR — it's already
|
||||
shown in the card above and would push the overlay too large. */
|
||||
.node-top-row .node-qr-wrap .mono { display: none; }
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* #1273 — QR overlay container is 2-3× taller than the QR canvas on the
|
||||
* full node detail page (`#/nodes/<pubkey>`).
|
||||
*
|
||||
* On mobile (<=640px) and desktop, `.node-top-row .node-qr-wrap` (the QR
|
||||
* overlay box) must NOT have meaningful empty translucent space below the
|
||||
* QR canvas. The wrap's bounding-rect height must be ≤ the inner QR
|
||||
* canvas/svg height + 32px (covers padding + caption + minor rounding).
|
||||
*
|
||||
* Asserted on:
|
||||
* - 375x800 (mobile — overlay style applies)
|
||||
* - 1280x800 (desktop guard — existing flex layout must not regress)
|
||||
*
|
||||
* RED on master (mobile): the absolute-positioned overlay inherits the
|
||||
* column flex layout with `justify-content: center` and the caption hidden
|
||||
* but still allocates space because the wrap doesn't have a content-fit
|
||||
* height.
|
||||
*
|
||||
* Usage: BASE_URL=http://localhost:13581 node test-issue-1273-qr-overlay-height-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 pickPubkey(page) {
|
||||
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
|
||||
return await page.evaluate(async () => {
|
||||
const r = await fetch('/api/nodes?limit=20');
|
||||
const d = await r.json();
|
||||
return (d.nodes || [])[0] && (d.nodes || [])[0].public_key;
|
||||
});
|
||||
}
|
||||
|
||||
async function measureOverlay(page, pubkey) {
|
||||
await page.goto(BASE + '/#/nodes/' + encodeURIComponent(pubkey),
|
||||
{ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('.node-top-row .node-qr-wrap', { timeout: 10000 });
|
||||
// Wait until the QR svg is actually painted inside the wrap.
|
||||
await page.waitForFunction(() => {
|
||||
const wrap = document.querySelector('.node-top-row .node-qr-wrap');
|
||||
return wrap && wrap.querySelector('.node-qr svg');
|
||||
}, { timeout: 10000 });
|
||||
await page.waitForTimeout(150); // allow layout to settle
|
||||
return await page.evaluate(() => {
|
||||
const wrap = document.querySelector('.node-top-row .node-qr-wrap');
|
||||
const svg = wrap.querySelector('.node-qr svg');
|
||||
const wr = wrap.getBoundingClientRect();
|
||||
const sr = svg.getBoundingClientRect();
|
||||
const cap = wrap.querySelector('.mono');
|
||||
const capH = cap && getComputedStyle(cap).display !== 'none'
|
||||
? Math.round(cap.getBoundingClientRect().height) : 0;
|
||||
// QR is always square — the visible/intended QR height is the SMALLER
|
||||
// of svg width vs svg height. Any "extra" svg height beyond that is
|
||||
// wasted intrinsic-sizing space that bloats the wrap.
|
||||
const qrVisibleH = Math.min(Math.round(sr.width), Math.round(sr.height));
|
||||
return {
|
||||
wrapH: Math.round(wr.height),
|
||||
wrapW: Math.round(wr.width),
|
||||
svgH: Math.round(sr.height),
|
||||
svgW: Math.round(sr.width),
|
||||
qrVisibleH,
|
||||
capH,
|
||||
position: getComputedStyle(wrap).position,
|
||||
top: Math.round(wr.top),
|
||||
right: Math.round(window.innerWidth - wr.right),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const launchOpts = { headless: true, args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] };
|
||||
if (process.env.CHROMIUM_PATH) launchOpts.executablePath = process.env.CHROMIUM_PATH;
|
||||
const browser = await chromium.launch(launchOpts);
|
||||
|
||||
console.log(`\n=== #1273 QR overlay height E2E against ${BASE} ===`);
|
||||
|
||||
// ── pick a real pubkey from the API ──
|
||||
const ctxBoot = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const probe = await ctxBoot.newPage();
|
||||
const pubkey = await pickPubkey(probe);
|
||||
await ctxBoot.close();
|
||||
assert(pubkey, 'No pubkey returned from /api/nodes');
|
||||
console.log(' → probe pubkey: ' + pubkey.slice(0, 12) + '…');
|
||||
|
||||
// ── Mobile 375x800 ──
|
||||
const m = await browser.newContext({ viewport: { width: 375, height: 800 } });
|
||||
const mp = await m.newPage();
|
||||
await step('mobile 375x800: .node-qr-wrap height \u2264 visible QR + 32px', async () => {
|
||||
const d = await measureOverlay(mp, pubkey);
|
||||
console.log(' mobile measurements: ' + JSON.stringify(d));
|
||||
assert(d.qrVisibleH > 0, 'QR svg has zero visible square (not rendered)');
|
||||
assert(d.position === 'absolute',
|
||||
'mobile overlay must remain position:absolute, got ' + d.position);
|
||||
assert(d.wrapH <= d.qrVisibleH + d.capH + 32,
|
||||
`wrap height ${d.wrapH}px must be \u2264 visible QR ${d.qrVisibleH}px + caption ${d.capH}px + 32px (delta ${d.wrapH - d.qrVisibleH - d.capH}px)`);
|
||||
});
|
||||
await m.close();
|
||||
|
||||
// ── Desktop 1280x800 (regression guard) ──
|
||||
const dctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const dp = await dctx.newPage();
|
||||
await step('desktop 1280x800: .node-qr-wrap height \u2264 visible QR + caption + 32px', async () => {
|
||||
const d = await measureOverlay(dp, pubkey);
|
||||
console.log(' desktop measurements: ' + JSON.stringify(d));
|
||||
assert(d.qrVisibleH > 0, 'QR svg has zero visible square (not rendered)');
|
||||
assert(d.wrapH <= d.qrVisibleH + d.capH + 32,
|
||||
`wrap height ${d.wrapH}px must be \u2264 visible QR ${d.qrVisibleH}px + caption ${d.capH}px + 32px (delta ${d.wrapH - d.qrVisibleH - d.capH}px)`);
|
||||
});
|
||||
await dctx.close();
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log('\n' + passed + '/' + (passed + failed) + ' tests passed' +
|
||||
(failed ? ', ' + failed + ' failed' : ''));
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
)().catch(err => { console.error('Fatal:', err); process.exit(1); });
|
||||
Reference in New Issue
Block a user