mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 13:33:06 +00:00
## Summary RED test commit: `02652d0042b7cf65d1f9b3e96ce376bbb3064ba6` — CI: https://github.com/Kpa-clawbot/CoreScope/actions Mobile UX overhaul for the Channels page (#1224). At 375x800 the sidebar header was 112px tall (title + button stacked, analytics link + region filter each on their own row) and the channel-name column was clipped to 83px by the inline 📤 Share + ✕ Remove buttons. ## What changed - **Header is now ONE row**: title + region filter + `+ Add` chip + `📊` analytics overflow chip. Capped to ≤56px on mobile. - **`+ Add Channel` → `+ Add` chip** (no longer a full-width hero). Verified <65% of sidebar width. - **Analytics link** is an icon-only chip inside the header (was a full-row link below). - **Region filter** is inline inside the header (was its own row). - **Channel rows**: `.ch-item-name` takes `flex:1`, share button is icon-only (📤), remove button shrunk to 32px touch target. Name >150px on the first row. - **Empty state** is `max-height:30vh; padding:12px` on mobile — no longer dominates the viewport. ## Design decisions - Chose **inline chips** over an overflow `⋮` menu: header-level controls are few enough (4) that stacking pills + filter dropdown fits comfortably in 375px. Avoids the cost/complexity of a popover and matches the page's existing pill vocabulary (region filter). - Per-row share/remove kept inline but icon-only (`font-size:0` + `::before`) — preserves single-tap access without consuming the row. - Touch targets stay ≥32px (action chips) / 44px (other tappables); WCAG 2.5.5 spirit retained on the dominant interactive paths. - **Desktop layout (≥768px) is unchanged** — verified by a desktop guard in the E2E (`.ch-layout` flex-direction stays `row` at 1024px). ## Tests - `test-issue-1224-channels-mobile-ux-e2e.js` — 5 assertions at 375x800 + 1 desktop guard at 1024x800. Wired into CI. - Existing channel suites still pass: `test-channel-fluid-e2e.js` (11/11), `test-channel-issue-1087-e2e.js` (3/3), `test-channel-issue-1111-e2e.js` (2/2), `test-channel-modal-ux.js` (33/33), `test-channel-ux-followup.js` (29/29), `test-channel-sidebar-layout.js` + `test-channel-fluid-layout.js` (14/14). Fixes #1224 --------- Co-authored-by: clawbot <clawbot@users.noreply.github.com>
This commit is contained in:
1 parent
24f277e5c6
commit
70855249c2
4 files changed
+170
-10
No files matched your search
@@ -258,6 +258,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-mql-leak-1180-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
+4
-4
@@ -673,13 +673,13 @@
|
||||
<div class="ch-sidebar" aria-label="Channel list">
|
||||
<div class="ch-sidebar-header">
|
||||
<div class="ch-sidebar-title"><span class="ch-icon">💬</span> Channels</div>
|
||||
<div id="chRegionFilter" class="region-filter-container ch-header-region"></div>
|
||||
<button type="button" id="chAddChannelBtn" class="ch-add-channel-btn"
|
||||
aria-label="Add channel" title="Add a channel — generate, paste a key, or monitor a hashtag">+ Add Channel</button>
|
||||
aria-label="Add channel" title="Add a channel — generate, paste a key, or monitor a hashtag">+ Add</button>
|
||||
<a href="#/analytics" class="ch-analytics-link"
|
||||
title="Open the Analytics page to see channel activity stats" aria-label="Channel Analytics">📊</a>
|
||||
</div>
|
||||
<a href="#/analytics" class="ch-analytics-link"
|
||||
title="Open the Analytics page to see channel activity stats">📊 Channel Analytics →</a>
|
||||
<div id="chAddStatus" class="ch-add-status" style="display:none"></div>
|
||||
<div id="chRegionFilter" class="region-filter-container" style="padding:0 8px"></div>
|
||||
<div class="ch-channel-list" id="chList" role="listbox" aria-label="Channels">
|
||||
<div class="ch-loading">Loading channels…</div>
|
||||
</div>
|
||||
|
||||
+47
-6
@@ -1111,11 +1111,17 @@ fieldset.mc-section legend.mc-label { padding: 0; }
|
||||
border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.ch-sidebar-header {
|
||||
padding: 14px 16px; border-bottom: 1px solid var(--border);
|
||||
padding: 8px 12px; border-bottom: 1px solid var(--border);
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
||||
}
|
||||
.ch-sidebar-title {
|
||||
display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 700; margin-bottom: 8px;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
font-size: 15px; font-weight: 700;
|
||||
flex: 1 1 auto; min-width: 0;
|
||||
/* Name dominates; no longer push other controls onto a new row */
|
||||
margin: 0;
|
||||
}
|
||||
.ch-header-region { margin: 0; padding: 0; flex: 0 0 auto; }
|
||||
.ch-encrypted-toggle {
|
||||
display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-muted);
|
||||
cursor: pointer; user-select: none; margin-bottom: 4px;
|
||||
@@ -1727,6 +1733,39 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
|
||||
.ch-back-btn { display: flex; }
|
||||
.ch-main-header { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* #1224 Channels mobile UX: compact header, dominant channel name,
|
||||
icon-only row actions, smaller empty state. */
|
||||
.ch-sidebar-header {
|
||||
padding: 6px 10px; gap: 6px;
|
||||
max-height: 56px;
|
||||
}
|
||||
.ch-sidebar-title { font-size: 14px; }
|
||||
.ch-sidebar-title .ch-icon { font-size: 16px; }
|
||||
.ch-add-channel-btn { padding: 4px 8px; font-size: 12px; }
|
||||
.ch-analytics-link { padding: 3px 7px; font-size: 13px; }
|
||||
/* Region filter shows just its compact pills/dropdown trigger in the header. */
|
||||
.ch-header-region .region-filter-bar { padding: 0; gap: 4px; }
|
||||
.ch-header-region .region-pill { padding: 2px 6px; font-size: 11px; }
|
||||
.ch-header-region .region-dropdown-trigger { padding: 3px 6px; font-size: 11px; }
|
||||
|
||||
/* Channel list row: name must dominate. The inline share/remove icons
|
||||
were eating >88px because of their 44x44 touch target — push them
|
||||
onto a per-row reveal pattern: show a single ⋮ trigger that toggles
|
||||
the action set. Until then, shrink them to icon-only and let the
|
||||
name grow via flex:1. */
|
||||
#chList .ch-item { padding: 10px 12px; gap: 10px; }
|
||||
#chList .ch-item-name { flex: 1 1 auto; min-width: 0; }
|
||||
#chList .ch-item-top { gap: 6px; }
|
||||
#chList .ch-icon-btn { min-width: 32px; min-height: 32px; padding: 2px 4px; }
|
||||
#chList .ch-share-btn { font-size: 0; }
|
||||
#chList .ch-share-btn::before { content: '📤'; font-size: 13px; }
|
||||
#chList .ch-remove-btn { font-size: 12px; padding: 2px 6px; }
|
||||
/* Hide the analytics-style time on mobile rows to free width for the name. */
|
||||
#chList .ch-item-time { font-size: 10px; }
|
||||
|
||||
/* Empty state must not dominate. */
|
||||
.ch-empty { padding: 12px; font-size: 13px; max-height: 30vh; }
|
||||
|
||||
/* Tables: smaller text for mobile */
|
||||
.data-table { font-size: 11px; min-width: 0; }
|
||||
.data-table td { padding: 5px 4px; max-width: 100px; }
|
||||
@@ -1919,12 +1958,14 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
|
||||
.ch-add-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; line-height: 1.3; }
|
||||
.ch-add-status { font-size: 12px; margin-top: 4px; padding: 4px 6px; border-radius: 4px; }
|
||||
.ch-analytics-link {
|
||||
display: block;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ch-analytics-link:hover { color: var(--accent); }
|
||||
.ch-add-status--loading { color: var(--text-muted); }
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* E2E (#1224): Channels page mobile UX overhaul.
|
||||
*
|
||||
* At 375x800 viewport the channels page must:
|
||||
* - Render a header strip above the channel list ≤60px tall (page title +
|
||||
* Add chip + region filter chip + analytics overflow) in ONE row.
|
||||
* - Render "+ Add Channel" as a compact chip — NOT a full-width hero (the
|
||||
* add control must be narrower than 65% of the sidebar width).
|
||||
* - Render channel rows where the channel name has computed-width > 150px
|
||||
* (the row must not be clipped by oversized inline action buttons).
|
||||
* - Render the "Select a channel" empty state container occupying < 40% of
|
||||
* the viewport height (no desktop-thinking empty state on mobile).
|
||||
*
|
||||
* Run: BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-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);
|
||||
const ctx = await browser.newContext({ viewport: { width: 375, height: 800 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
await page.goto(BASE + '/#/channels', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#chList', { timeout: 10000 });
|
||||
await page.waitForFunction(() => {
|
||||
const l = document.getElementById('chList');
|
||||
return l && l.querySelectorAll('.ch-item').length > 0;
|
||||
}, { timeout: 15000 });
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await step('header strip above channel list is \u226460px tall on mobile', async () => {
|
||||
const headerH = await page.evaluate(() => {
|
||||
const sidebar = document.querySelector('.ch-sidebar');
|
||||
const header = sidebar && sidebar.querySelector('.ch-sidebar-header');
|
||||
if (!header) return null;
|
||||
return Math.round(header.getBoundingClientRect().height);
|
||||
});
|
||||
assert(headerH !== null, 'sidebar header not found');
|
||||
assert(headerH <= 60, 'sidebar header must be \u226460px on mobile, got ' + headerH + 'px');
|
||||
});
|
||||
|
||||
await step('"+ Add Channel" is a compact chip, not full-width hero', async () => {
|
||||
const ratio = await page.evaluate(() => {
|
||||
const sidebar = document.querySelector('.ch-sidebar');
|
||||
const btn = document.getElementById('chAddChannelBtn');
|
||||
if (!sidebar || !btn) return null;
|
||||
const sw = sidebar.getBoundingClientRect().width;
|
||||
const bw = btn.getBoundingClientRect().width;
|
||||
return bw / sw;
|
||||
});
|
||||
assert(ratio !== null, 'add channel button not found');
|
||||
assert(ratio < 0.65, 'add button width must be <65% of sidebar, got ratio=' + ratio.toFixed(2));
|
||||
});
|
||||
|
||||
await step('first channel row name has computed-width >150px', async () => {
|
||||
const nameW = await page.evaluate(() => {
|
||||
const name = document.querySelector('#chList .ch-item .ch-item-name');
|
||||
if (!name) return null;
|
||||
return Math.round(name.getBoundingClientRect().width);
|
||||
});
|
||||
assert(nameW !== null, 'first channel name element not found');
|
||||
assert(nameW > 150, 'channel name width must be >150px on mobile, got ' + nameW + 'px');
|
||||
});
|
||||
|
||||
await step('empty-state container is < 40% of viewport height', async () => {
|
||||
const data = await page.evaluate(() => {
|
||||
const empty = document.querySelector('.ch-empty');
|
||||
if (!empty) return null;
|
||||
return {
|
||||
h: Math.round(empty.getBoundingClientRect().height),
|
||||
vh: window.innerHeight,
|
||||
};
|
||||
});
|
||||
assert(data !== null, 'empty state element not found');
|
||||
const pct = data.h / data.vh;
|
||||
assert(pct < 0.40,
|
||||
'empty-state height ' + data.h + 'px is ' + Math.round(pct * 100) +
|
||||
'% of viewport (' + data.vh + 'px) \u2014 must be <40%');
|
||||
});
|
||||
|
||||
// Desktop guard: at 1024x800 the sidebar must remain side-by-side with main
|
||||
// (layout flex-direction stays row), not stacked. This protects the desktop
|
||||
// experience from a regression introduced by the mobile fix.
|
||||
await ctx.close();
|
||||
const ctx2 = await browser.newContext({ viewport: { width: 1024, height: 800 } });
|
||||
const p2 = await ctx2.newPage();
|
||||
await p2.goto(BASE + '/#/channels', { waitUntil: 'domcontentloaded' });
|
||||
await p2.waitForSelector('.ch-layout', { timeout: 10000 });
|
||||
await p2.waitForTimeout(200);
|
||||
|
||||
await step('desktop (1024px): ch-layout stays row (side-by-side)', async () => {
|
||||
const dir = await p2.evaluate(() => {
|
||||
const l = document.querySelector('.ch-layout');
|
||||
return l ? getComputedStyle(l).flexDirection : null;
|
||||
});
|
||||
assert(dir === 'row', 'desktop ch-layout flex-direction must be "row", got ' + dir);
|
||||
});
|
||||
|
||||
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); });
|
||||
Reference in new issue
Block a user