feat: reorganize settings tabs, right-align device labels, activity dots, sidebar glow, display config toggles

This commit is contained in:
Brian Krabach
2026-04-01 06:23:23 -07:00
parent 20eee04e46
commit e71ac223a9
8 changed files with 257 additions and 62 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+70 -24
View File
@@ -146,6 +146,10 @@ const DISPLAY_DEFAULTS = {
bellSound: false,
notificationPermission: 'default',
viewMode: 'auto',
showDeviceBadges: true, // show device name labels on tiles/sidebar
showActivityGlow: true, // show amber border/glow on tiles/sidebar with bells
showHoverPreview: true, // show hover preview popover on tile hover
showActivityDot: true, // show activity dot on tiles/sidebar
};
var VIEW_MODES = ['auto', 'fit'];
@@ -455,38 +459,42 @@ function buildTileHTML(session, index, mobile) {
const isBell = priority === 'bell';
const unseen = session.bell && session.bell.unseen_count;
var ds = loadDisplaySettings();
let classes = 'session-tile';
if (isBell) classes += ' session-tile--bell';
if (isBell && ds.showActivityGlow !== false) classes += ' session-tile--bell';
if (mobile) classes += ` session-tile--tier-${priority}`;
const name = session.name || '';
const escapedName = escapeHtml(name);
const timeStr = formatTimestamp(session.last_activity_at || null);
// Bell indicator
let bellHtml = '';
if (unseen && unseen > 0) {
const countStr = unseen > 9 ? '9+' : String(unseen);
bellHtml = `<span class="tile-bell">${countStr}</span>`;
// Activity dot — absolute positioned in upper-right corner of tile (no count text)
let bellDotHtml = '';
if (unseen && unseen > 0 && ds.showActivityDot !== false) {
bellDotHtml = '<span class="tile-bell-dot"></span>';
}
// Device badge (shown when multiple sources configured and session has a device name)
const badgeHtml = _sources.length > 1 && session.deviceName
? `<span class="device-badge">${escapeHtml(session.deviceName)}</span>`
: '';
// Device badge — right-aligned in header, separate from name span
// Shown when multiple sources configured AND session has a device name
let badgeHtml = '';
if (_sources.length > 1 && session.deviceName && ds.showDeviceBadges !== false) {
badgeHtml = `<span class="device-badge">${escapeHtml(session.deviceName)}</span>`;
}
// Last N lines of snapshot — show more in fit mode so tall tiles fill
const snapshot = session.snapshot || '';
var _tileDs = loadDisplaySettings();
var _lineCount = (_tileDs.viewMode === 'fit') ? -80 : -20;
var _lineCount = (ds.viewMode === 'fit') ? -80 : -20;
const lastLines = snapshot.split('\n').slice(_lineCount).join('\n');
const sourceUrlAttr = session.sourceUrl ? ` data-source-url="${escapeHtml(session.sourceUrl)}"` : '';
return (
`<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">` +
`<span class="tile-name">${escapeHtml(name)}${badgeHtml}</span>` +
`<span class="tile-meta">${bellHtml}<span class="tile-time">${escapeHtml(timeStr)}</span></span>` +
`<span class="tile-name">${escapeHtml(name)}</span>` +
badgeHtml +
`<span class="tile-meta"><span class="tile-time">${escapeHtml(timeStr)}</span></span>` +
`</div>` +
`<div class="tile-body"><pre>${ansiToHtml(lastLines)}</pre></div>` +
`<button class="tile-delete" data-session="${escapedName}" aria-label="Kill session">&times;</button>` +
@@ -505,16 +513,25 @@ function buildSidebarHTML(session, currentSession) {
const escapedName = escapeHtml(name);
const isActive = name === currentSession;
let classes = 'sidebar-item';
if (isActive) classes += ' sidebar-item--active';
var ds = loadDisplaySettings();
const unseen = session.bell && session.bell.unseen_count;
const isBell = unseen && unseen > 0;
// Bell indicator
let bellHtml = '';
if (unseen && unseen > 0) {
const countStr = unseen > 9 ? '9+' : String(unseen);
bellHtml = `<span class="tile-bell">${countStr}</span>`;
let classes = 'sidebar-item';
if (isActive) classes += ' sidebar-item--active';
if (isBell && ds.showActivityGlow !== false) classes += ' sidebar-item--bell';
// Activity dot — inline in sidebar header (not absolute, since items are smaller)
let bellDotHtml = '';
if (isBell && ds.showActivityDot !== false) {
bellDotHtml = '<span class="tile-bell-dot sidebar-bell-dot"></span>';
}
// Device badge — between name and bell/delete, pushed right via flex
let badgeHtml = '';
if (_sources.length > 1 && session.deviceName && ds.showDeviceBadges !== false) {
badgeHtml = `<span class="device-badge">${escapeHtml(session.deviceName)}</span>`;
}
// Last 20 lines of snapshot
@@ -524,8 +541,9 @@ function buildSidebarHTML(session, currentSession) {
return (
`<article class="${classes}" data-session="${escapedName}" data-source-url="${escapeHtml(session.sourceUrl || '')}" tabindex="0" role="listitem">` +
`<div class="sidebar-item-header">` +
`<span class="sidebar-item-name">${escapedName}${_sources.length > 1 && session.deviceName ? '<span class="device-badge">' + escapeHtml(session.deviceName) + '</span>' : ''}</span>` +
`${bellHtml}` +
`<span class="sidebar-item-name">${escapedName}</span>` +
badgeHtml +
bellDotHtml +
`<button class="sidebar-delete" data-session="${escapedName}" aria-label="Kill session">&times;</button>` +
`</div>` +
`<div class="sidebar-item-body"><pre>${ansiToHtml(lastLines)}</pre></div>` +
@@ -955,6 +973,8 @@ function _previewClickHandler(e) {
function showPreview(name) {
if (!name || !_currentSessions) return;
var _previewDs = loadDisplaySettings();
if (_previewDs.showHoverPreview === false) return;
var session = _currentSessions.find(function (s) { return s.name === name; });
if (!session || !session.snapshot) return;
@@ -1657,6 +1677,18 @@ function onDisplaySettingChange() {
ds.gridColumns = raw === 'auto' ? 'auto' : parseInt(raw, 10);
}
var showDeviceBadgesEl = document.getElementById('setting-show-device-badges');
if (showDeviceBadgesEl) ds.showDeviceBadges = showDeviceBadgesEl.checked;
var showActivityGlowEl = document.getElementById('setting-show-activity-glow');
if (showActivityGlowEl) ds.showActivityGlow = showActivityGlowEl.checked;
var showHoverPreviewEl = document.getElementById('setting-show-hover-preview');
if (showHoverPreviewEl) ds.showHoverPreview = showHoverPreviewEl.checked;
var showActivityDotEl = document.getElementById('setting-show-activity-dot');
if (showActivityDotEl) ds.showActivityDot = showActivityDotEl.checked;
saveDisplaySettings(ds);
applyDisplaySettings(ds);
}
@@ -1705,7 +1737,17 @@ function openSettings() {
const viewModeEl = $('setting-view-mode');
if (viewModeEl) viewModeEl.value = loadGridViewMode();
// Populate Notifications tab from display settings
// Populate new display toggle checkboxes
const showDeviceBadgesEl = $('setting-show-device-badges');
if (showDeviceBadgesEl) showDeviceBadgesEl.checked = settings.showDeviceBadges !== false;
const showActivityGlowEl = $('setting-show-activity-glow');
if (showActivityGlowEl) showActivityGlowEl.checked = settings.showActivityGlow !== false;
const showHoverPreviewEl = $('setting-show-hover-preview');
if (showHoverPreviewEl) showHoverPreviewEl.checked = settings.showHoverPreview !== false;
const showActivityDotEl = $('setting-show-activity-dot');
if (showActivityDotEl) showActivityDotEl.checked = settings.showActivityDot !== false;
// Populate Sessions tab / bell sound from display settings
const bellSoundEl = $('setting-bell-sound');
if (bellSoundEl) bellSoundEl.checked = !!settings.bellSound;
@@ -2227,6 +2269,10 @@ function bindStaticEventListeners() {
on($('setting-font-size'), 'change', onDisplaySettingChange);
on($('setting-hover-delay'), 'change', onDisplaySettingChange);
on($('setting-grid-columns'), 'change', onDisplaySettingChange);
on($('setting-show-device-badges'), 'change', onDisplaySettingChange);
on($('setting-show-activity-glow'), 'change', onDisplaySettingChange);
on($('setting-show-hover-preview'), 'change', onDisplaySettingChange);
on($('setting-show-activity-dot'), 'change', onDisplaySettingChange);
on($('setting-view-mode'), 'change', function() {
var el = $('setting-view-mode');
if (el) {
+28 -22
View File
@@ -16,7 +16,7 @@
</head>
<body>
<!-- ── Overview view ─────────────────────────────────────────────────────── -->
<!-- ── Overview view ──────────────────────────────────────────────────── -->
<div id="view-overview" class="view view--active">
<header class="app-header">
<h1 class="app-wordmark"><img src="/wordmark-on-dark.svg" alt="muxplex" height="24" /></h1>
@@ -32,7 +32,7 @@
<div id="empty-state" class="empty-state hidden">No active tmux sessions</div>
</div>
<!-- ── Expanded (terminal) view ──────────────────────────────────────────── -->
<!-- ── Expanded (terminal) view ──────────────────────────────────────── -->
<div id="view-expanded" class="view hidden">
<header class="expanded-header">
<button id="back-btn" class="back-btn" aria-label="Back">&#8592;</button>
@@ -56,7 +56,7 @@
<div id="reconnect-overlay" class="reconnect-overlay hidden" aria-live="polite">Reconnecting&hellip;</div>
</div>
<!-- ── Bottom sheet (session switcher) ───────────────────────────────────── -->
<!-- ── Bottom sheet (session switcher) ────────────────────────────────── -->
<div id="bottom-sheet" class="bottom-sheet hidden" role="dialog" aria-modal="true" aria-label="Switch session">
<div class="bottom-sheet__backdrop" id="sheet-backdrop"></div>
<div class="bottom-sheet__panel">
@@ -65,19 +65,19 @@
</div>
</div>
<!-- ── Session pill (persistent overlay button) ──────────────────────────── -->
<!-- ── Session pill (persistent overlay button) ────────────────────────── -->
<button id="session-pill" class="session-pill hidden" aria-label="Switch session">
<span id="session-pill-label" class="session-pill__label"></span>
<span id="session-pill-bell" class="session-pill__bell hidden" aria-hidden="true">&#128276;</span>
</button>
<!-- ── Mobile FAB (new session) ──────────────────────────────────────────── -->
<!-- ── Mobile FAB (new session) ──────────────────────────────────────────── -->
<button id="new-session-fab" class="new-session-fab" aria-label="New session">+</button>
<!-- ── Toast notification ─────────────────────────────────────────────────── -->
<!-- ── Toast notification ──────────────────────────────────────────────── -->
<div id="toast" class="toast hidden" role="status" aria-live="polite" aria-atomic="true"></div>
<!-- ── Settings ─────────────────────────────────────────────────────────────────────── -->
<!-- ── Settings ──────────────────────────────────────────────────────────── -->
<div id="settings-backdrop" class="settings-backdrop hidden"></div>
<dialog id="settings-dialog" class="settings-dialog">
<div class="settings-dialog-header">
@@ -88,7 +88,6 @@
<nav class="settings-tabs">
<button class="settings-tab settings-tab--active" data-tab="display">Display</button>
<button class="settings-tab" data-tab="sessions">Sessions</button>
<button class="settings-tab" data-tab="notifications">Notifications</button>
<button class="settings-tab" data-tab="new-session">Commands</button>
<button class="settings-tab" data-tab="devices">Multi-Device</button>
</nav>
@@ -124,6 +123,26 @@
<option value="4">4</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-show-device-badges">Show device badges</label>
<input type="checkbox" id="setting-show-device-badges" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-show-activity-glow">Show activity glow</label>
<input type="checkbox" id="setting-show-activity-glow" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-show-hover-preview">Show hover preview</label>
<input type="checkbox" id="setting-show-hover-preview" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-show-activity-dot">Show activity dot</label>
<input type="checkbox" id="setting-show-activity-dot" class="settings-checkbox" checked />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
</div>
<div class="settings-panel hidden" data-tab="sessions">
<div class="settings-field">
@@ -152,8 +171,6 @@
<label class="settings-label" for="setting-auto-open">Auto-open created sessions</label>
<input type="checkbox" id="setting-auto-open" class="settings-checkbox" checked />
</div>
</div>
<div class="settings-panel hidden" data-tab="notifications">
<div class="settings-field">
<label class="settings-label" for="setting-bell-sound">Bell Sound</label>
<input type="checkbox" id="setting-bell-sound" class="settings-checkbox" />
@@ -185,10 +202,6 @@
<label class="settings-label" for="setting-multi-device-enabled">Enable multi-device support</label>
<input type="checkbox" id="setting-multi-device-enabled" class="settings-checkbox" />
</div>
<div class="settings-field">
<label class="settings-label" for="setting-device-name">Device Name</label>
<input type="text" id="setting-device-name" class="settings-input" placeholder="This device" />
</div>
<div id="multi-device-fields">
<div class="settings-field">
<label class="settings-label" for="setting-view-mode">View Mode</label>
@@ -198,13 +211,6 @@
<option value="filtered">Filtered</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-view-scope">View Scope</label>
<select id="setting-view-scope" class="settings-select">
<option value="all">All Devices</option>
<option value="local">Local Only</option>
</select>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Remote Instances</label>
<div id="setting-remote-instances" class="settings-remote-list"></div>
@@ -216,7 +222,7 @@
</div>
</dialog>
<!-- ── Scripts ─────────────────────────────────────────────────────────────── -->
<!-- ── Scripts ──────────────────────────────────────────────────────────── -->
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<script src="/app.js" defer></script>
+27 -7
View File
@@ -499,6 +499,12 @@ body {
border-left: 3px solid var(--accent);
}
/* Bell / activity glow — matches .session-tile--bell */
.sidebar-item--bell {
border-color: var(--bell-border);
box-shadow: 0 0 0 1px var(--bell-border), inset 0 0 12px var(--bell-glow);
}
.sidebar-item-header {
display: flex;
flex-direction: row;
@@ -605,15 +611,29 @@ body {
}
/* ============================================================
Bell count badge
Activity dot — absolute positioned in upper-right corner of tile
============================================================ */
.tile-bell-count {
font-size: 10px;
font-weight: 600;
color: var(--bell);
min-width: 16px;
text-align: right;
.tile-bell-dot {
position: absolute;
top: 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;
}
/* ============================================================
+132 -9
View File
@@ -451,24 +451,26 @@ test('buildTileHTML sets data-session attribute with escaped session name', () =
assert.ok(html.includes('data-session="my&lt;session&gt;"'), 'data-session should be escaped');
});
test('buildTileHTML shows 9+ when unseen_count exceeds 9', () => {
test('buildTileHTML shows tile-bell-dot (not count) when unseen_count exceeds 9', () => {
const session = {
name: 's',
bell: { unseen_count: 10, seen_at: null, last_fired_at: 100 },
snapshot: '',
};
const html = app.buildTileHTML(session, 0, false);
assert.ok(html.includes('9+'), 'should show 9+ for count > 9');
assert.ok(html.includes('tile-bell-dot'), 'should show tile-bell-dot for count > 9');
assert.ok(!html.includes('9+'), 'should NOT show 9+ count text (dot only)');
});
test('buildTileHTML shows exact count when unseen_count is <= 9', () => {
test('buildTileHTML shows tile-bell-dot (not count) when unseen_count is <= 9', () => {
const session = {
name: 's',
bell: { unseen_count: 5, seen_at: null, last_fired_at: 100 },
snapshot: '',
};
const html = app.buildTileHTML(session, 0, false);
assert.ok(html.includes('>5<'), 'should show exact count 5');
assert.ok(html.includes('tile-bell-dot'), 'should show tile-bell-dot for count > 0');
assert.ok(!html.includes('>5<'), 'should NOT show exact count 5 (dot only)');
});
test('buildTileHTML includes only last 20 lines of snapshot', () => {
@@ -1544,17 +1546,17 @@ 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');
});
test('buildSidebarHTML renders bell badge with tile-bell class and count when unseen_count > 0', () => {
test('buildSidebarHTML renders bell dot indicator when unseen_count > 0', () => {
const session = { name: 's', snapshot: '', bell: { unseen_count: 3 } };
const html = app.buildSidebarHTML(session, '');
assert.ok(html.includes('tile-bell'), 'should contain tile-bell class when unseen_count > 0');
assert.ok(html.includes('>3<'), 'should contain unseen count text');
assert.ok(html.includes('tile-bell-dot'), 'should contain tile-bell-dot class when unseen_count > 0');
assert.ok(!html.includes('>3<'), 'should NOT contain unseen count text (dot only)');
});
test('buildSidebarHTML omits bell badge when unseen_count is 0', () => {
test('buildSidebarHTML omits bell dot when unseen_count is 0', () => {
const session = { name: 's', snapshot: '', bell: { unseen_count: 0 } };
const html = app.buildSidebarHTML(session, '');
assert.ok(!html.includes('tile-bell'), 'should not contain tile-bell class when unseen_count is 0');
assert.ok(!html.includes('tile-bell-dot'), 'should not contain tile-bell-dot when unseen_count is 0');
});
test('buildSidebarHTML HTML-escapes session name to prevent XSS', () => {
@@ -3827,3 +3829,124 @@ test('bindStaticEventListeners does NOT bind change event to setting-view-scope'
);
});
// ─── Activity dot / sidebar glow / config toggles (UI/UX improvements) ───────
test('buildTileHTML bell dot is positioned before tile-header (absolute overlay)', () => {
const session = {
name: 's',
bell: { unseen_count: 1, seen_at: null, last_fired_at: 100 },
snapshot: '',
};
const html = app.buildTileHTML(session, 0, false);
const dotIdx = html.indexOf('tile-bell-dot');
const headerIdx = html.indexOf('tile-header');
assert.ok(dotIdx !== -1, 'tile-bell-dot must be present');
assert.ok(dotIdx < headerIdx, 'tile-bell-dot must appear before tile-header in HTML');
});
test('buildTileHTML omits tile-bell-dot when unseen_count is 0', () => {
const session = { name: 's', bell: { unseen_count: 0 }, snapshot: '' };
const html = app.buildTileHTML(session, 0, false);
assert.ok(!html.includes('tile-bell-dot'), 'no tile-bell-dot when unseen_count is 0');
});
test('buildSidebarHTML adds sidebar-item--bell class for bell sessions', () => {
const session = { name: 's', snapshot: '', bell: { unseen_count: 3, seen_at: null, last_fired_at: 100 } };
const html = app.buildSidebarHTML(session, '');
assert.ok(html.includes('sidebar-item--bell'), 'sidebar item should have sidebar-item--bell class when bell is active');
});
test('buildSidebarHTML does not add sidebar-item--bell when no unseen', () => {
const session = { name: 's', snapshot: '', bell: { unseen_count: 0 } };
const html = app.buildSidebarHTML(session, '');
assert.ok(!html.includes('sidebar-item--bell'), 'should NOT have sidebar-item--bell when unseen_count is 0');
});
test('DISPLAY_DEFAULTS includes showDeviceBadges key', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const defaultsStart = source.indexOf('DISPLAY_DEFAULTS');
const defaultsEnd = source.indexOf('};', defaultsStart);
const defaultsBody = source.substring(defaultsStart, defaultsEnd + 2);
assert.ok(defaultsBody.includes('showDeviceBadges'), 'DISPLAY_DEFAULTS must include showDeviceBadges');
});
test('DISPLAY_DEFAULTS includes showActivityGlow key', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const defaultsStart = source.indexOf('DISPLAY_DEFAULTS');
const defaultsEnd = source.indexOf('};', defaultsStart);
const defaultsBody = source.substring(defaultsStart, defaultsEnd + 2);
assert.ok(defaultsBody.includes('showActivityGlow'), 'DISPLAY_DEFAULTS must include showActivityGlow');
});
test('DISPLAY_DEFAULTS includes showHoverPreview key', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const defaultsStart = source.indexOf('DISPLAY_DEFAULTS');
const defaultsEnd = source.indexOf('};', defaultsStart);
const defaultsBody = source.substring(defaultsStart, defaultsEnd + 2);
assert.ok(defaultsBody.includes('showHoverPreview'), 'DISPLAY_DEFAULTS must include showHoverPreview');
});
test('DISPLAY_DEFAULTS includes showActivityDot key', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const defaultsStart = source.indexOf('DISPLAY_DEFAULTS');
const defaultsEnd = source.indexOf('};', defaultsStart);
const defaultsBody = source.substring(defaultsStart, defaultsEnd + 2);
assert.ok(defaultsBody.includes('showActivityDot'), 'DISPLAY_DEFAULTS must include showActivityDot');
});
test('HTML index.html has setting-show-device-badges checkbox', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(source.includes('setting-show-device-badges'), 'Display panel must have setting-show-device-badges checkbox');
});
test('HTML index.html has setting-show-activity-glow checkbox', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(source.includes('setting-show-activity-glow'), 'Display panel must have setting-show-activity-glow checkbox');
});
test('HTML index.html has setting-show-hover-preview checkbox', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(source.includes('setting-show-hover-preview'), 'Display panel must have setting-show-hover-preview checkbox');
});
test('HTML index.html has setting-show-activity-dot checkbox', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(source.includes('setting-show-activity-dot'), 'Display panel must have setting-show-activity-dot checkbox');
});
test('CSS style.css has .tile-bell-dot rule', () => {
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
assert.ok(source.includes('.tile-bell-dot'), 'style.css must have .tile-bell-dot rule');
});
test('CSS style.css has .sidebar-item--bell rule', () => {
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
assert.ok(source.includes('.sidebar-item--bell'), 'style.css must have .sidebar-item--bell rule');
});
test('CSS style.css has .sidebar-bell-dot rule', () => {
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
assert.ok(source.includes('.sidebar-bell-dot'), 'style.css must have .sidebar-bell-dot rule');
});
test('showPreview checks showHoverPreview setting before showing popover', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const fnStart = source.indexOf('function showPreview(');
assert.ok(fnStart !== -1, 'showPreview must exist');
const fnEnd = source.indexOf('\n}', fnStart);
const fnBody = source.substring(fnStart, fnEnd + 2);
assert.ok(fnBody.includes('showHoverPreview'), 'showPreview must check showHoverPreview setting before showing popover');
});
test('bindStaticEventListeners binds change events for new display toggle checkboxes', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const fnStart = source.indexOf('function bindStaticEventListeners(');
assert.ok(fnStart !== -1, 'bindStaticEventListeners must exist');
const fnEnd = source.indexOf('\nfunction ', fnStart + 1);
const fnBody = source.substring(fnStart, fnEnd > fnStart ? fnEnd : fnStart + 10000);
assert.ok(fnBody.includes('setting-show-device-badges'), 'must bind setting-show-device-badges');
assert.ok(fnBody.includes('setting-show-activity-glow'), 'must bind setting-show-activity-glow');
assert.ok(fnBody.includes('setting-show-hover-preview'), 'must bind setting-show-hover-preview');
assert.ok(fnBody.includes('setting-show-activity-dot'), 'must bind setting-show-activity-dot');
});