mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-20 20:20:56 +00:00
## Summary Partial fix for #1770 (S quick-fix path only; L refactor remains as follow-up). The packets-view virtual-scroller assumes a constant `VSCROLL_ROW_HEIGHT`, but the base rule at `public/style.css` L1097 lets `td.col-details` wrap on narrow viewports (`white-space: normal; word-break: break-word`). Wrapped rows produce variable row heights → visible jitter when scrolling past ~900px on iOS. **Quick-fix (S path):** under the existing `@media (max-width: 640px)` block in `public/style.css`, clamp `.col-details` to a single line: ```css .data-table td.col-details { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ``` Trade-off accepted in triage: Details column truncates on mobile in exchange for smooth scrolling. The base rule keeps wrapping on desktop (≥641px) so nothing changes there. **Out of scope:** the full L-path fix (per-row measurement, `_rowHeightsPx[]`, cumulative offsets, re-measure on hop-resolver finalize) — tracked separately on #1770. ## TDD - **Red commit** `7f58bedc` — adds `test-issue-1770-mobile-row-clamp.js`, a CSS-grep test (same pattern as `test-issue-1364-pill-no-clamp.js`) that walks every `@media (max-width: 640px)` block in `public/style.css` and asserts a `.col-details` rule declares `white-space: nowrap`, `overflow: hidden`, and `text-overflow: ellipsis`. Verified to FAIL on master (assertion failure, not a parse error) and PASS after the CSS change. - **Green commit** `d46271b8` — applies the 5-line CSS clamp inside the existing mobile breakpoint at L2362. ## Files touched - `public/style.css` (+13) - `test-issue-1770-mobile-row-clamp.js` (+101, new) ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` → all gates pass (PII, branch scope, red commit, css-vars, css self-fallback, LIKE-on-JSON, sync migration, async-migration, XSS). No warnings. --------- Co-authored-by: clawbot <bot@clawbot.local> Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
co-authored by
clawbot
openclaw-bot
parent
b3189c613a
commit
707d70c738
@@ -157,6 +157,7 @@ jobs:
|
||||
node test-a11y-axe-1668-selftest.js
|
||||
node test-a11y-1716-rf-range-btn-active.js
|
||||
node test-issue-1705-subpath-contrast.js
|
||||
node test-issue-1770-mobile-row-clamp.js
|
||||
node test-a11y-axe-routes-coverage.js
|
||||
|
||||
- name: 🛡️ Preflight XSS gate — actual --diff check (PR only)
|
||||
|
||||
+3
-3
@@ -2252,7 +2252,7 @@
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(headerObserverId) || '')}">${isSingle ? escapeHtml(truncate(obsNameOnly(headerObserverId), 16)) + obsIataBadge(p) : escapeHtml(truncate(obsNameOnly(headerObserverId), 10)) + groupedObserverIataBadgesHtml(p)}</td>
|
||||
<td class="col-path"><span class="path-hops">${groupPathStr}</span></td>
|
||||
<td class="col-rpt">${p.observation_count > 1 ? '<span class="badge badge-obs" title="Seen ' + p.observation_count + ' times"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-eye"/></svg> ' + p.observation_count + '</span>' : (isSingle ? '' : p.count)}</td>
|
||||
<td class="col-details">${getDetailPreview(getParsedDecoded(p))}</td>
|
||||
<td class="col-details"><span class="col-details-clip">${getDetailPreview(getParsedDecoded(p))}</span></td>
|
||||
</tr>`;
|
||||
if (isExpanded && p._children) {
|
||||
let visibleChildren = p._children;
|
||||
@@ -2281,7 +2281,7 @@
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(c.observer_id) || '')}">${escapeHtml(truncate(obsNameOnly(c.observer_id), 16))}${obsIataBadge(c)}</td>
|
||||
<td class="col-path"><span class="path-hops">${childPathStr}</span></td>
|
||||
<td class="col-rpt"></td>
|
||||
<td class="col-details">${getDetailPreview(getParsedDecoded(c))}</td>
|
||||
<td class="col-details"><span class="col-details-clip">${getDetailPreview(getParsedDecoded(c))}</span></td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
@@ -2314,7 +2314,7 @@
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(p.observer_id) || '')}">${escapeHtml(truncate(obsNameOnly(p.observer_id), 16))}${obsIataBadge(p)}</td>
|
||||
<td class="col-path"><span class="path-hops">${pathStr}</span></td>
|
||||
<td class="col-rpt"></td>
|
||||
<td class="col-details">${detail}</td>
|
||||
<td class="col-details"><span class="col-details-clip">${detail}</span></td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -2433,6 +2433,32 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
|
||||
.data-table .col-time { min-width: 64px; }
|
||||
.panel-left { overflow-x: auto; }
|
||||
|
||||
/* #1770: packets-view virtual-scroller assumes constant row height
|
||||
(VSCROLL_ROW_HEIGHT). The base rule at ~L1097 sets
|
||||
`.data-table td.col-details { white-space: normal; word-break: break-word }`
|
||||
which lets the "Details" cell wrap on narrow viewports, producing
|
||||
variable row heights → visible jitter when scrolling past ~900px on
|
||||
iOS. Quick-fix (S path): clamp the cell to a single line on mobile.
|
||||
The proper L-path fix (per-row measurement) tracks separately.
|
||||
|
||||
v2 (#1805 follow-up): the clamp lives on an INNER `.col-details-clip`
|
||||
wrapper, not the `<td>` itself. Reason: with `white-space: nowrap` on
|
||||
the td and `table-layout: auto`, the cell's min-content becomes the
|
||||
full single-line text width, blowing past the `max-width: 100px`
|
||||
mobile hint. Rows then grow wider than the 360px viewport — which
|
||||
breaks `test-gestures-1062-e2e.js` (a)/(h) because the swipe coords
|
||||
fall outside the viewport and `elementFromPoint` no longer hits the
|
||||
row. Inline-block + fixed `max-width` on the clip caps the column's
|
||||
preferred width so the row stays viewport-bounded and gestures work. */
|
||||
.data-table td.col-details > .col-details-clip {
|
||||
display: inline-block;
|
||||
max-width: 140px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/* Filters: collapse on mobile */
|
||||
.filter-bar { flex-direction: row; flex-wrap: wrap; gap: 4px; }
|
||||
.filter-toggle-btn { display: inline-flex !important; }
|
||||
|
||||
@@ -63,6 +63,7 @@ node test-issue-1532-live-fullscreen.js
|
||||
node test-naive-banner-tone.js
|
||||
node test-issue-1473-reserved-prefixes.js
|
||||
node test-issue-1473-prefix-generator.js
|
||||
node test-issue-1770-mobile-row-clamp.js
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* #1770 — Packets view jitters on mobile because variable row heights
|
||||
* break the virtual-scroller's constant-row-height assumption.
|
||||
*
|
||||
* S quick-fix path: under the mobile breakpoint, clamp .col-details
|
||||
* (the packet "Details" cell in the packets table) to a single line via
|
||||
* white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
* The base rule at line ~1097 currently sets `white-space: normal` which
|
||||
* allows wrapping → variable row heights → virtual-scroll jitter.
|
||||
*
|
||||
* This is a CSS-grep test: it parses public/style.css, locates the
|
||||
* `@media (max-width: 640px)` block used by the packets view on mobile,
|
||||
* and asserts that block declares a `.col-details` clamp rule with
|
||||
* `white-space: nowrap`. Cheap to run, no browser required.
|
||||
*
|
||||
* Pattern borrowed from test-issue-1364-pill-no-clamp.js.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
||||
else { failed++; console.error(' ✗ ' + msg); }
|
||||
}
|
||||
|
||||
const cssSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
|
||||
|
||||
// Pull the existing mobile breakpoint block. The packets-table mobile
|
||||
// overrides (.data-table font-size, .data-table td max-width, etc.) live
|
||||
// inside `@media (max-width: 640px)` at ~line 2361.
|
||||
// Pull every `@media (max-width: 640px)` block. There are several in
|
||||
// style.css (the packets-view block is at ~L2362 but many feature
|
||||
// sections add their own); the clamp can live in any of them.
|
||||
function extractAllMediaBlocks(src, header) {
|
||||
const out = [];
|
||||
let from = 0;
|
||||
while (true) {
|
||||
const idx = src.indexOf(header, from);
|
||||
if (idx === -1) break;
|
||||
let depth = 0, started = false, end = -1;
|
||||
for (let i = idx; i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (c === '{') { depth++; started = true; }
|
||||
else if (c === '}') {
|
||||
depth--;
|
||||
if (started && depth === 0) { end = i + 1; break; }
|
||||
}
|
||||
}
|
||||
if (end === -1) break;
|
||||
out.push(src.slice(idx, end));
|
||||
from = end;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const mobileBlocks = extractAllMediaBlocks(cssSrc, '@media (max-width: 640px)');
|
||||
|
||||
console.log('\n=== #1770: .col-details clamps to one line under mobile breakpoint ===');
|
||||
|
||||
assert(mobileBlocks.length > 0, '`@media (max-width: 640px)` block(s) found in style.css');
|
||||
|
||||
if (mobileBlocks.length > 0) {
|
||||
// Find the .col-details rule body inside any mobile block. Accepts any
|
||||
// selector that includes `.col-details` (e.g. `.col-details { ... }` or
|
||||
// `.data-table td.col-details { ... }` or — post-#1805 — an inner
|
||||
// `.col-details > .col-details-clip` wrapper). The clamp invariant is
|
||||
// what matters; the selector scope was tightened in #1805 to avoid
|
||||
// blowing past `max-width` and breaking row-action gestures (#1062).
|
||||
const ruleRe = /([^{}]*\.col-details\b[^{}]*)\{([^{}]*)\}/g;
|
||||
let clampBody = null;
|
||||
for (const block of mobileBlocks) {
|
||||
let m;
|
||||
ruleRe.lastIndex = 0;
|
||||
while ((m = ruleRe.exec(block)) !== null) {
|
||||
if (/white-space\s*:\s*nowrap/.test(m[2])) { clampBody = m[2]; break; }
|
||||
}
|
||||
if (clampBody) break;
|
||||
}
|
||||
|
||||
assert(
|
||||
!!clampBody,
|
||||
'.col-details mobile rule declares `white-space: nowrap` (single-line clamp)'
|
||||
);
|
||||
|
||||
if (clampBody) {
|
||||
assert(
|
||||
/overflow\s*:\s*hidden/.test(clampBody),
|
||||
'.col-details mobile rule declares `overflow: hidden`'
|
||||
);
|
||||
assert(
|
||||
/text-overflow\s*:\s*ellipsis/.test(clampBody),
|
||||
'.col-details mobile rule declares `text-overflow: ellipsis`'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Summary ===');
|
||||
console.log(' Passed: ' + passed);
|
||||
console.log(' Failed: ' + failed);
|
||||
console.log('\n#1770 ' + (failed === 0 ? 'PASS' : 'FAIL'));
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user