feat: edge-bar state indicator, stacked sidebar header, × crossfade on hover
This commit is contained in:
+15
-17
@@ -501,25 +501,21 @@ function ansi256Color(n) {
|
|||||||
function buildTileHTML(session, index, mobile) {
|
function buildTileHTML(session, index, mobile) {
|
||||||
const priority = sessionPriority(session);
|
const priority = sessionPriority(session);
|
||||||
const isBell = priority === 'bell';
|
const isBell = priority === 'bell';
|
||||||
const unseen = session.bell && session.bell.unseen_count;
|
|
||||||
|
|
||||||
var ds = loadDisplaySettings();
|
var ds = loadDisplaySettings();
|
||||||
var actIndicator = ds.activityIndicator !== undefined ? ds.activityIndicator : 'both';
|
var actIndicator = ds.activityIndicator !== undefined ? ds.activityIndicator : 'both';
|
||||||
|
|
||||||
let classes = 'session-tile';
|
let classes = 'session-tile';
|
||||||
|
// Glow (full border + inner glow): applied when actIndicator is 'glow' or 'both'
|
||||||
if (isBell && (actIndicator === 'glow' || actIndicator === 'both')) classes += ' session-tile--bell';
|
if (isBell && (actIndicator === 'glow' || actIndicator === 'both')) classes += ' session-tile--bell';
|
||||||
|
// Edge bar only (left border amber, no glow): applied when actIndicator is 'dot' or 'both'
|
||||||
|
if (isBell && (actIndicator === 'dot' || actIndicator === 'both')) classes += ' session-tile--edge-bell';
|
||||||
if (mobile) classes += ` session-tile--tier-${priority}`;
|
if (mobile) classes += ` session-tile--tier-${priority}`;
|
||||||
|
|
||||||
const name = session.name || '';
|
const name = session.name || '';
|
||||||
const escapedName = escapeHtml(name);
|
const escapedName = escapeHtml(name);
|
||||||
const timeStr = formatTimestamp(session.last_activity_at || null);
|
const timeStr = formatTimestamp(session.last_activity_at || null);
|
||||||
|
|
||||||
// Activity dot — absolute positioned in lower-right corner of tile (no count text)
|
|
||||||
let bellDotHtml = '';
|
|
||||||
if (unseen && unseen > 0 && (actIndicator === 'dot' || actIndicator === 'both')) {
|
|
||||||
bellDotHtml = '<span class="tile-bell-dot"></span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device badge — right-aligned in header, separate from name span
|
// Device badge — right-aligned in header, separate from name span
|
||||||
// Shown when multiple sources configured AND session has a device name
|
// Shown when multiple sources configured AND session has a device name
|
||||||
let badgeHtml = '';
|
let badgeHtml = '';
|
||||||
@@ -535,7 +531,6 @@ function buildTileHTML(session, index, mobile) {
|
|||||||
const sourceUrlAttr = session.sourceUrl ? ` data-source-url="${escapeHtml(session.sourceUrl)}"` : '';
|
const sourceUrlAttr = session.sourceUrl ? ` data-source-url="${escapeHtml(session.sourceUrl)}"` : '';
|
||||||
return (
|
return (
|
||||||
`<article class="${classes}" data-session="${escapedName}" data-session-key="${escapeHtml(session.sessionKey || name)}"${sourceUrlAttr} tabindex="0" role="listitem" aria-label="${escapedName}">` +
|
`<article class="${classes}" data-session="${escapedName}" data-session-key="${escapeHtml(session.sessionKey || name)}"${sourceUrlAttr} tabindex="0" role="listitem" aria-label="${escapedName}">` +
|
||||||
bellDotHtml +
|
|
||||||
`<div class="tile-header">` +
|
`<div class="tile-header">` +
|
||||||
`<span class="tile-name">${escapeHtml(name)}</span>` +
|
`<span class="tile-name">${escapeHtml(name)}</span>` +
|
||||||
badgeHtml +
|
badgeHtml +
|
||||||
@@ -566,20 +561,20 @@ function buildSidebarHTML(session, currentSession) {
|
|||||||
|
|
||||||
let classes = 'sidebar-item';
|
let classes = 'sidebar-item';
|
||||||
if (isActive) classes += ' sidebar-item--active';
|
if (isActive) classes += ' sidebar-item--active';
|
||||||
|
// Glow (full border + inner glow): applied when actIndicator is 'glow' or 'both'
|
||||||
if (isBell && (actIndicator === 'glow' || actIndicator === 'both')) classes += ' sidebar-item--bell';
|
if (isBell && (actIndicator === 'glow' || actIndicator === 'both')) classes += ' sidebar-item--bell';
|
||||||
|
// Edge bar only (left border amber, no glow): applied when actIndicator is 'dot' or 'both'
|
||||||
|
if (isBell && (actIndicator === 'dot' || actIndicator === 'both')) classes += ' sidebar-item--edge-bell';
|
||||||
|
|
||||||
// Activity dot — inline in sidebar header (not absolute, since items are smaller)
|
// Device badge — shown in meta line when multiple sources configured
|
||||||
let bellDotHtml = '';
|
|
||||||
if (isBell && (actIndicator === 'dot' || actIndicator === 'both')) {
|
|
||||||
bellDotHtml = '<span class="tile-bell-dot sidebar-bell-dot"></span>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device badge — between name and bell/delete, pushed right via flex
|
|
||||||
let badgeHtml = '';
|
let badgeHtml = '';
|
||||||
if (_sources.length > 1 && session.deviceName && ds.showDeviceBadges !== false) {
|
if (_sources.length > 1 && session.deviceName && ds.showDeviceBadges !== false) {
|
||||||
badgeHtml = `<span class="device-badge">${escapeHtml(session.deviceName)}</span>`;
|
badgeHtml = `<span class="device-badge">${escapeHtml(session.deviceName)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timestamp for meta line
|
||||||
|
const timeStr = formatTimestamp(session.last_activity_at || null);
|
||||||
|
|
||||||
// Last 20 lines of snapshot
|
// Last 20 lines of snapshot
|
||||||
const snapshot = session.snapshot || '';
|
const snapshot = session.snapshot || '';
|
||||||
const lastLines = snapshot.split('\n').slice(-20).join('\n');
|
const lastLines = snapshot.split('\n').slice(-20).join('\n');
|
||||||
@@ -588,10 +583,13 @@ function buildSidebarHTML(session, currentSession) {
|
|||||||
`<article class="${classes}" data-session="${escapedName}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem">` +
|
`<article class="${classes}" data-session="${escapedName}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem">` +
|
||||||
`<div class="sidebar-item-header">` +
|
`<div class="sidebar-item-header">` +
|
||||||
`<span class="sidebar-item-name">${escapedName}</span>` +
|
`<span class="sidebar-item-name">${escapedName}</span>` +
|
||||||
badgeHtml +
|
|
||||||
bellDotHtml +
|
|
||||||
`<button class="sidebar-delete" data-session="${escapedName}" aria-label="Kill session">×</button>` +
|
`<button class="sidebar-delete" data-session="${escapedName}" aria-label="Kill session">×</button>` +
|
||||||
`</div>` +
|
`</div>` +
|
||||||
|
`<div class="sidebar-item-meta">` +
|
||||||
|
badgeHtml +
|
||||||
|
`<span class="sidebar-meta-sep">\xb7</span>` +
|
||||||
|
`<span class="sidebar-item-time">${escapeHtml(timeStr)}</span>` +
|
||||||
|
`</div>` +
|
||||||
`<div class="sidebar-item-body"><pre>${ansiToHtml(lastLines)}</pre></div>` +
|
`<div class="sidebar-item-body"><pre>${ansiToHtml(lastLines)}</pre></div>` +
|
||||||
`</article>`
|
`</article>`
|
||||||
);
|
);
|
||||||
|
|||||||
+50
-29
@@ -171,13 +171,14 @@ body {
|
|||||||
height: var(--tile-height);
|
height: var(--tile-height);
|
||||||
background: var(--bg-tile);
|
background: var(--bg-tile);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid transparent; /* edge bar — always present, transparent by default */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: border-color var(--t-fast), box-shadow var(--t-fast);
|
transition: border-color var(--t-fast), border-left-color var(--t-fast), box-shadow var(--t-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-tile:hover,
|
.session-tile:hover,
|
||||||
@@ -191,6 +192,11 @@ body {
|
|||||||
box-shadow: 0 0 0 1px var(--bell-border), inset 0 0 12px var(--bell-glow);
|
box-shadow: 0 0 0 1px var(--bell-border), inset 0 0 12px var(--bell-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edge-bar only (dot mode): amber left border, no glow */
|
||||||
|
.session-tile--edge-bell {
|
||||||
|
border-left-color: var(--bell);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Tile header
|
Tile header
|
||||||
============================================================ */
|
============================================================ */
|
||||||
@@ -221,6 +227,16 @@ body {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* × crossfade with timestamp on hover */
|
||||||
|
.tile-time {
|
||||||
|
transition: opacity 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-tile:hover .tile-time,
|
||||||
|
.session-tile:focus-within .tile-time {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Bell notification badge */
|
/* Bell notification badge */
|
||||||
.tile-bell {
|
.tile-bell {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -521,8 +537,9 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid transparent; /* edge bar — always present, transparent by default */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: border-color var(--t-fast), box-shadow var(--t-fast);
|
transition: border-color var(--t-fast), border-left-color var(--t-fast), box-shadow var(--t-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item:hover,
|
.sidebar-item:hover,
|
||||||
@@ -542,6 +559,11 @@ body {
|
|||||||
box-shadow: 0 0 0 1px var(--bell-border), inset 0 0 12px var(--bell-glow);
|
box-shadow: 0 0 0 1px var(--bell-border), inset 0 0 12px var(--bell-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edge-bar only (dot mode): amber left border, no glow */
|
||||||
|
.sidebar-item--edge-bell {
|
||||||
|
border-left-color: var(--bell);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-item-header {
|
.sidebar-item-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -587,6 +609,21 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar meta line — device badge + separator + timestamp */
|
||||||
|
.sidebar-item-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 8px 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-meta-sep {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-empty {
|
.sidebar-empty {
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -647,31 +684,11 @@ body {
|
|||||||
transition: opacity var(--t-fade);
|
transition: opacity var(--t-fade);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* Activity dot removed — edge bar (border-left-color) is now the state indicator.
|
||||||
Activity dot — absolute positioned in upper-right corner of tile
|
.tile-bell-dot and .sidebar-bell-dot have been replaced by:
|
||||||
============================================================ */
|
.session-tile--edge-bell { border-left-color: var(--bell); }
|
||||||
|
.sidebar-item--edge-bell { border-left-color: var(--bell); }
|
||||||
.tile-bell-dot {
|
*/
|
||||||
position: absolute;
|
|
||||||
bottom: 6px;
|
|
||||||
right: 6px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--bell);
|
|
||||||
animation: bell-pulse 1.4s ease-in-out infinite;
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar variant: inline (not absolute), fits within header row */
|
|
||||||
.sidebar-bell-dot {
|
|
||||||
position: static;
|
|
||||||
display: inline-block;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Connection status indicator states
|
Connection status indicator states
|
||||||
@@ -1046,8 +1063,8 @@ body {
|
|||||||
|
|
||||||
.tile-delete {
|
.tile-delete {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 8px;
|
||||||
right: 6px;
|
right: 8px;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
@@ -1057,6 +1074,7 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 150ms ease;
|
transition: opacity 150ms ease;
|
||||||
|
pointer-events: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -1064,6 +1082,7 @@ body {
|
|||||||
.session-tile:hover .tile-delete,
|
.session-tile:hover .tile-delete,
|
||||||
.session-tile:focus-within .tile-delete {
|
.session-tile:focus-within .tile-delete {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-delete:hover {
|
.tile-delete:hover {
|
||||||
@@ -1082,6 +1101,7 @@ body {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 150ms ease;
|
transition: opacity 150ms ease;
|
||||||
|
pointer-events: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
@@ -1089,6 +1109,7 @@ body {
|
|||||||
.sidebar-item:hover .sidebar-delete,
|
.sidebar-item:hover .sidebar-delete,
|
||||||
.sidebar-item:focus-within .sidebar-delete {
|
.sidebar-item:focus-within .sidebar-delete {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-delete:hover {
|
.sidebar-delete:hover {
|
||||||
|
|||||||
@@ -451,26 +451,26 @@ test('buildTileHTML sets data-session attribute with escaped session name', () =
|
|||||||
assert.ok(html.includes('data-session="my<session>"'), 'data-session should be escaped');
|
assert.ok(html.includes('data-session="my<session>"'), 'data-session should be escaped');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML shows tile-bell-dot (not count) when unseen_count exceeds 9', () => {
|
test('buildTileHTML shows edge-bell class (not count text) when unseen_count exceeds 9', () => {
|
||||||
const session = {
|
const session = {
|
||||||
name: 's',
|
name: 's',
|
||||||
bell: { unseen_count: 10, seen_at: null, last_fired_at: 100 },
|
bell: { unseen_count: 10, seen_at: null, last_fired_at: 100 },
|
||||||
snapshot: '',
|
snapshot: '',
|
||||||
};
|
};
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('tile-bell-dot'), 'should show tile-bell-dot for count > 9');
|
assert.ok(html.includes('session-tile--edge-bell'), 'should have edge-bell class for count > 9');
|
||||||
assert.ok(!html.includes('9+'), 'should NOT show 9+ count text (dot only)');
|
assert.ok(!html.includes('9+'), 'should NOT show 9+ count text (edge bar only)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML shows tile-bell-dot (not count) when unseen_count is <= 9', () => {
|
test('buildTileHTML shows edge-bell class (not count text) when unseen_count is <= 9', () => {
|
||||||
const session = {
|
const session = {
|
||||||
name: 's',
|
name: 's',
|
||||||
bell: { unseen_count: 5, seen_at: null, last_fired_at: 100 },
|
bell: { unseen_count: 5, seen_at: null, last_fired_at: 100 },
|
||||||
snapshot: '',
|
snapshot: '',
|
||||||
};
|
};
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('tile-bell-dot'), 'should show tile-bell-dot for count > 0');
|
assert.ok(html.includes('session-tile--edge-bell'), 'should have edge-bell class for count > 0');
|
||||||
assert.ok(!html.includes('>5<'), 'should NOT show exact count 5 (dot only)');
|
assert.ok(!html.includes('>5<'), 'should NOT show exact count 5 (edge bar only)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML includes only last 20 lines of snapshot', () => {
|
test('buildTileHTML includes only last 20 lines of snapshot', () => {
|
||||||
@@ -1546,17 +1546,19 @@ test('buildSidebarHTML does not add sidebar-item--active class for inactive sess
|
|||||||
assert.ok(!html.includes('sidebar-item--active'), 'inactive session should not have sidebar-item--active class');
|
assert.ok(!html.includes('sidebar-item--active'), 'inactive session should not have sidebar-item--active class');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML renders bell dot indicator when unseen_count > 0', () => {
|
test('buildSidebarHTML shows bell indicator class when unseen_count > 0', () => {
|
||||||
const session = { name: 's', snapshot: '', bell: { unseen_count: 3 } };
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 3 } };
|
||||||
const html = app.buildSidebarHTML(session, '');
|
const html = app.buildSidebarHTML(session, '');
|
||||||
assert.ok(html.includes('tile-bell-dot'), 'should contain tile-bell-dot class when unseen_count > 0');
|
// activityIndicator defaults to 'both' so sidebar-item--bell and sidebar-item--edge-bell should appear
|
||||||
assert.ok(!html.includes('>3<'), 'should NOT contain unseen count text (dot only)');
|
assert.ok(html.includes('sidebar-item--bell'), 'should have sidebar-item--bell class when unseen_count > 0');
|
||||||
|
assert.ok(!html.includes('>3<'), 'should NOT contain unseen count text (edge bar only)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML omits bell dot when unseen_count is 0', () => {
|
test('buildSidebarHTML omits bell indicator classes when unseen_count is 0', () => {
|
||||||
const session = { name: 's', snapshot: '', bell: { unseen_count: 0 } };
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 0 } };
|
||||||
const html = app.buildSidebarHTML(session, '');
|
const html = app.buildSidebarHTML(session, '');
|
||||||
assert.ok(!html.includes('tile-bell-dot'), 'should not contain tile-bell-dot when unseen_count is 0');
|
assert.ok(!html.includes('sidebar-item--bell'), 'should not have sidebar-item--bell when unseen_count is 0');
|
||||||
|
assert.ok(!html.includes('sidebar-item--edge-bell'), 'should not have sidebar-item--edge-bell when unseen_count is 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML HTML-escapes session name to prevent XSS', () => {
|
test('buildSidebarHTML HTML-escapes session name to prevent XSS', () => {
|
||||||
@@ -3831,23 +3833,26 @@ test('bindStaticEventListeners does NOT bind change event to setting-view-scope'
|
|||||||
|
|
||||||
// ─── Activity dot / sidebar glow / config toggles (UI/UX improvements) ───────
|
// ─── Activity dot / sidebar glow / config toggles (UI/UX improvements) ───────
|
||||||
|
|
||||||
test('buildTileHTML bell dot is positioned before tile-header (absolute overlay)', () => {
|
test('buildTileHTML edge-bar: session-tile--edge-bell class on article element (not a separate DOM element)', () => {
|
||||||
const session = {
|
const session = {
|
||||||
name: 's',
|
name: 's',
|
||||||
bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 },
|
bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 },
|
||||||
snapshot: '',
|
snapshot: '',
|
||||||
};
|
};
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
const dotIdx = html.indexOf('tile-bell-dot');
|
// Edge bar is CSS-only (border-left-color on the article element), no separate DOM element
|
||||||
const headerIdx = html.indexOf('tile-header');
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must be on the article element');
|
||||||
assert.ok(dotIdx !== -1, 'tile-bell-dot must be present');
|
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT be present — replaced by edge bar CSS');
|
||||||
assert.ok(dotIdx < headerIdx, 'tile-bell-dot must appear before tile-header in HTML');
|
// The class must appear on the opening article tag, not buried in content
|
||||||
|
const articleTag = html.substring(0, html.indexOf('>'));
|
||||||
|
assert.ok(articleTag.includes('session-tile--edge-bell'), 'edge-bell class must be on the article element');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML omits tile-bell-dot when unseen_count is 0', () => {
|
test('buildTileHTML omits bell indicator classes when unseen_count is 0', () => {
|
||||||
const session = { name: 's', bell: { unseen_count: 0 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 0 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(!html.includes('tile-bell-dot'), 'no tile-bell-dot when unseen_count is 0');
|
assert.ok(!html.includes('tile-bell-dot'), 'no tile-bell-dot when unseen_count is 0');
|
||||||
|
assert.ok(!html.includes('session-tile--edge-bell'), 'no session-tile--edge-bell when unseen_count is 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildSidebarHTML adds sidebar-item--bell class for bell sessions', () => {
|
test('buildSidebarHTML adds sidebar-item--bell class for bell sessions', () => {
|
||||||
@@ -3969,35 +3974,39 @@ test('HTML index.html has setting-activity-indicator select element', () => {
|
|||||||
assert.ok(source.includes('value="none"'), 'must have None option');
|
assert.ok(source.includes('value="none"'), 'must have None option');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML shows tile-bell-dot when activityIndicator is dot', () => {
|
test('buildTileHTML shows session-tile--edge-bell class when activityIndicator is dot (legacy test updated)', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'dot' });
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'dot' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('tile-bell-dot'), 'tile-bell-dot must appear when activityIndicator is dot');
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is dot');
|
||||||
|
assert.ok(!html.includes('session-tile--bell'), 'session-tile--bell (glow) must NOT appear when activityIndicator is dot only');
|
||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML shows tile-bell-dot when activityIndicator is both', () => {
|
test('buildTileHTML shows both session-tile--bell and session-tile--edge-bell when activityIndicator is both (legacy test updated)', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(html.includes('tile-bell-dot'), 'tile-bell-dot must appear when activityIndicator is both');
|
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is both');
|
||||||
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is both');
|
||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML omits tile-bell-dot when activityIndicator is none', () => {
|
test('buildTileHTML omits all bell indicator classes when activityIndicator is none', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'none' });
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'none' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear when activityIndicator is none');
|
assert.ok(!html.includes('session-tile--bell'), 'session-tile--bell must NOT appear when activityIndicator is none');
|
||||||
|
assert.ok(!html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must NOT appear when activityIndicator is none');
|
||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildTileHTML omits tile-bell-dot when activityIndicator is glow', () => {
|
test('buildTileHTML omits session-tile--edge-bell when activityIndicator is glow (glow only, no edge bar)', () => {
|
||||||
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'glow' });
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'glow' });
|
||||||
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
const html = app.buildTileHTML(session, 0, false);
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear when activityIndicator is glow');
|
assert.ok(html.includes('session-tile--bell'), 'session-tile--bell must appear when activityIndicator is glow');
|
||||||
|
assert.ok(!html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must NOT appear when activityIndicator is glow');
|
||||||
_localStorageStore = {};
|
_localStorageStore = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4031,15 +4040,15 @@ test('CSS style.css has #setting-device-name max-width rule', () => {
|
|||||||
assert.ok(source.includes('max-width'), 'style.css #setting-device-name rule must include max-width');
|
assert.ok(source.includes('max-width'), 'style.css #setting-device-name rule must include max-width');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('CSS style.css .tile-bell-dot uses bottom positioning not top', () => {
|
test('CSS style.css .session-tile--edge-bell sets border-left-color to var(--bell)', () => {
|
||||||
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||||
// Find the .tile-bell-dot rule block
|
// Find the .session-tile--edge-bell rule block
|
||||||
const ruleStart = source.indexOf('.tile-bell-dot {');
|
const ruleStart = source.indexOf('.session-tile--edge-bell {');
|
||||||
assert.ok(ruleStart !== -1, '.tile-bell-dot rule must exist');
|
assert.ok(ruleStart !== -1, '.session-tile--edge-bell rule must exist');
|
||||||
const ruleEnd = source.indexOf('}', ruleStart);
|
const ruleEnd = source.indexOf('}', ruleStart);
|
||||||
const ruleBody = source.substring(ruleStart, ruleEnd + 1);
|
const ruleBody = source.substring(ruleStart, ruleEnd + 1);
|
||||||
assert.ok(ruleBody.includes('bottom:') || ruleBody.includes('bottom :'), '.tile-bell-dot must use bottom positioning');
|
assert.ok(ruleBody.includes('border-left-color') || ruleBody.includes('border-left'), '.session-tile--edge-bell must set border-left-color');
|
||||||
assert.ok(!ruleBody.includes('top: 6px'), '.tile-bell-dot must NOT use top: 6px');
|
assert.ok(ruleBody.includes('var(--bell)'), '.session-tile--edge-bell must use var(--bell) color');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('HTML Display panel device name field appears before font size field', () => {
|
test('HTML Display panel device name field appears before font size field', () => {
|
||||||
@@ -4153,3 +4162,128 @@ test('index.html contains popup federation auth relay script', () => {
|
|||||||
assert.ok(source.includes('muxplex-auth-token'), 'index.html must post muxplex-auth-token message type');
|
assert.ok(source.includes('muxplex-auth-token'), 'index.html must post muxplex-auth-token message type');
|
||||||
assert.ok(source.includes('/api/auth/token'), 'index.html popup script must fetch /api/auth/token');
|
assert.ok(source.includes('/api/auth/token'), 'index.html popup script must fetch /api/auth/token');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Edge-bar design: failing tests added before implementation ───
|
||||||
|
|
||||||
|
test('buildTileHTML does NOT include tile-bell-dot in HTML (edge bar replaces dot)', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
||||||
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
|
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear in HTML — edge bar replaces it');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildTileHTML adds session-tile--edge-bell class when activityIndicator is dot', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'dot' });
|
||||||
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is dot');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildTileHTML adds session-tile--edge-bell class when activityIndicator is both', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
||||||
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
|
assert.ok(html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must appear when activityIndicator is both');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildTileHTML does NOT add session-tile--edge-bell when activityIndicator is glow', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'glow' });
|
||||||
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
|
assert.ok(!html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must NOT appear when activityIndicator is glow');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildTileHTML does NOT add session-tile--edge-bell when activityIndicator is none', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'none' });
|
||||||
|
const session = { name: 's', bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 }, snapshot: '' };
|
||||||
|
const html = app.buildTileHTML(session, 0, false);
|
||||||
|
assert.ok(!html.includes('session-tile--edge-bell'), 'session-tile--edge-bell must NOT appear when activityIndicator is none');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML has sidebar-item-meta line (two-line header)', () => {
|
||||||
|
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 }, last_activity_at: null };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('sidebar-item-meta'), 'sidebar-item-meta element must exist in sidebar HTML');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML sidebar-item-meta contains sidebar-meta-sep dot separator', () => {
|
||||||
|
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 }, last_activity_at: null };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('sidebar-meta-sep'), 'sidebar-meta-sep must be present in sidebar HTML');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML sidebar-item-meta contains sidebar-item-time', () => {
|
||||||
|
const session = { name: 'my-session', snapshot: '', bell: { unseen_count: 0 }, last_activity_at: null };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('sidebar-item-time'), 'sidebar-item-time must be present in sidebar HTML');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML adds sidebar-item--edge-bell when activityIndicator is dot', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'dot' });
|
||||||
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must appear when activityIndicator is dot');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML adds sidebar-item--edge-bell when activityIndicator is both', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
||||||
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must appear when activityIndicator is both');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML does NOT add sidebar-item--edge-bell when activityIndicator is glow', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'glow' });
|
||||||
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(!html.includes('sidebar-item--edge-bell'), 'sidebar-item--edge-bell must NOT appear when activityIndicator is glow');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSidebarHTML does NOT include tile-bell-dot in HTML', () => {
|
||||||
|
_localStorageStore['muxplex.display'] = JSON.stringify({ activityIndicator: 'both' });
|
||||||
|
const session = { name: 's', snapshot: '', bell: { unseen_count: 2 } };
|
||||||
|
const html = app.buildSidebarHTML(session, '');
|
||||||
|
assert.ok(!html.includes('tile-bell-dot'), 'tile-bell-dot must NOT appear in sidebar HTML — edge bar replaces it');
|
||||||
|
_localStorageStore = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CSS style.css has .session-tile--edge-bell rule', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||||
|
assert.ok(source.includes('.session-tile--edge-bell'), 'style.css must have .session-tile--edge-bell rule');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CSS style.css has .sidebar-item--edge-bell rule', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||||
|
assert.ok(source.includes('.sidebar-item--edge-bell'), 'style.css must have .sidebar-item--edge-bell rule');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CSS style.css .session-tile has border-left for edge bar', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||||
|
const tileStart = source.indexOf('.session-tile {');
|
||||||
|
assert.ok(tileStart !== -1, '.session-tile rule must exist');
|
||||||
|
const tileEnd = source.indexOf('}', tileStart);
|
||||||
|
const tileBody = source.substring(tileStart, tileEnd + 1);
|
||||||
|
assert.ok(tileBody.includes('border-left'), '.session-tile must have border-left for edge bar');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CSS style.css has .sidebar-item-meta rule', () => {
|
||||||
|
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
|
||||||
|
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', () => {
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user