From f18caf5d2734a1d9dc464b30e6dae30bd3cfc2a7 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Wed, 1 Apr 2026 16:21:57 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20move=20device=20badge=20inside=20tile-me?= =?UTF-8?q?ta=20to=20prevent=20=C3=97=20overlap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- muxplex/frontend/app.js | 3 +- muxplex/frontend/style.css | 12 ++++--- muxplex/frontend/tests/test_app.mjs | 50 ++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index 0f74e4b..1442a62 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -488,8 +488,7 @@ function buildTileHTML(session, index, mobile) { `
` + `
` + `${escapeHtml(name)}` + - badgeHtml + - `${escapeHtml(timeStr)}` + + `${badgeHtml}${badgeHtml ? `\xb7` : ''}${escapeHtml(timeStr)}` + `
` + `
${ansiToHtml(lastLines)}
` + `` + diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index dadf16e..98a81e8 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -227,13 +227,17 @@ body { margin-left: 8px; } -/* × crossfade with timestamp on hover */ -.tile-time { +.tile-meta-sep { + margin: 0 2px; +} + +/* × crossfade with badge + timestamp on hover */ +.tile-meta { transition: opacity 150ms ease; } -.session-tile:hover .tile-time, -.session-tile:focus-within .tile-time { +.session-tile:hover .tile-meta, +.session-tile:focus-within .tile-meta { opacity: 0; } diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 62a0ebe..d9dc757 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2515,6 +2515,40 @@ test('buildTileHTML escapes HTML in deviceName within device-badge', () => { app._setServerSettings(null); }); +// --- buildTileHTML device badge placement (task-3) --- + +test('buildTileHTML places device-badge inside tile-meta span', () => { + app._setServerSettings({ multi_device_enabled: true }); + const session = { name: 'work', deviceName: 'Laptop', sessionKey: '::work', snapshot: '' }; + const html = app.buildTileHTML(session, 0, false); + const tileMetaStart = html.indexOf(''); + const tileMetaEnd = html.indexOf('', tileMetaStart); + assert.ok(tileMetaStart !== -1, 'tile-meta span should exist'); + const deviceBadgePos = html.indexOf('device-badge'); + assert.ok( + deviceBadgePos > tileMetaStart && deviceBadgePos < tileMetaEnd, + `device-badge should be inside tile-meta span (tile-meta starts at ${tileMetaStart}, device-badge at ${deviceBadgePos}, tile-meta closes at ${tileMetaEnd})` + ); + app._setServerSettings(null); +}); + +test('buildTileHTML includes tile-meta-sep with middle dot when badge present', () => { + app._setServerSettings({ multi_device_enabled: true }); + const session = { name: 'work', deviceName: 'Laptop', sessionKey: '::work', snapshot: '' }; + const html = app.buildTileHTML(session, 0, false); + assert.ok(html.includes('tile-meta-sep'), 'should include tile-meta-sep element when badge is present'); + assert.ok(html.includes('\u00b7'), 'should include middle dot separator (\u00b7)'); + app._setServerSettings(null); +}); + +test('buildTileHTML does not include tile-meta-sep when no badge', () => { + app._setServerSettings({ multi_device_enabled: false }); + const session = { name: 'work', deviceName: 'Laptop', sessionKey: '::work', snapshot: '' }; + const html = app.buildTileHTML(session, 0, false); + assert.ok(!html.includes('tile-meta-sep'), 'should NOT include tile-meta-sep when no badge'); + app._setServerSettings(null); +}); + // --- renderGrid grouped mode (task-11) --- @@ -3951,13 +3985,21 @@ test('CSS style.css has .sidebar-item-meta rule', () => { assert.ok(source.includes('.sidebar-item-meta'), 'style.css must have .sidebar-item-meta rule'); }); -test('CSS style.css .tile-time has opacity transition for crossfade', () => { +test('CSS style.css .tile-meta has opacity transition for crossfade (badge + timestamp together)', () => { const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8'); - assert.ok(source.includes('.tile-time'), 'style.css must have .tile-time rule'); assert.ok( - source.includes("session-tile:hover .tile-time"), - 'style.css must have session-tile:hover .tile-time for crossfade' + source.includes("session-tile:hover .tile-meta"), + 'style.css must have session-tile:hover .tile-meta for crossfade' ); + assert.ok( + source.includes("session-tile:focus-within .tile-meta"), + 'style.css must have session-tile:focus-within .tile-meta for crossfade' + ); +}); + +test('CSS style.css has .tile-meta-sep style', () => { + const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8'); + assert.ok(source.includes('.tile-meta-sep'), 'style.css must have .tile-meta-sep rule'); }); // --- Trailing blank line trimming in snapshot previews ---