fix: settings item ordering, device name width, activity indicator mode, dot position

This commit is contained in:
Brian Krabach
2026-04-01 06:52:01 -07:00
parent 50ecb1025d
commit a862327e27
4 changed files with 177 additions and 58 deletions
+14 -19
View File
@@ -147,9 +147,8 @@ const DISPLAY_DEFAULTS = {
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
activityIndicator: 'both', // 'none' | 'glow' | 'dot' | 'both'
};
var VIEW_MODES = ['auto', 'fit'];
@@ -460,18 +459,19 @@ function buildTileHTML(session, index, mobile) {
const unseen = session.bell && session.bell.unseen_count;
var ds = loadDisplaySettings();
var actIndicator = ds.activityIndicator !== undefined ? ds.activityIndicator : 'both';
let classes = 'session-tile';
if (isBell && ds.showActivityGlow !== false) classes += ' session-tile--bell';
if (isBell && (actIndicator === 'glow' || actIndicator === 'both')) 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);
// Activity dot — absolute positioned in upper-right corner of tile (no count text)
// Activity dot — absolute positioned in lower-right corner of tile (no count text)
let bellDotHtml = '';
if (unseen && unseen > 0 && ds.showActivityDot !== false) {
if (unseen && unseen > 0 && (actIndicator === 'dot' || actIndicator === 'both')) {
bellDotHtml = '<span class="tile-bell-dot"></span>';
}
@@ -514,17 +514,18 @@ function buildSidebarHTML(session, currentSession) {
const isActive = name === currentSession;
var ds = loadDisplaySettings();
var actIndicator = ds.activityIndicator !== undefined ? ds.activityIndicator : 'both';
const unseen = session.bell && session.bell.unseen_count;
const isBell = unseen && unseen > 0;
let classes = 'sidebar-item';
if (isActive) classes += ' sidebar-item--active';
if (isBell && ds.showActivityGlow !== false) classes += ' sidebar-item--bell';
if (isBell && (actIndicator === 'glow' || actIndicator === 'both')) classes += ' sidebar-item--bell';
// Activity dot — inline in sidebar header (not absolute, since items are smaller)
let bellDotHtml = '';
if (isBell && ds.showActivityDot !== false) {
if (isBell && (actIndicator === 'dot' || actIndicator === 'both')) {
bellDotHtml = '<span class="tile-bell-dot sidebar-bell-dot"></span>';
}
@@ -1680,14 +1681,11 @@ function onDisplaySettingChange() {
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;
var activityIndicatorEl = document.getElementById('setting-activity-indicator');
if (activityIndicatorEl) ds.activityIndicator = activityIndicatorEl.value;
saveDisplaySettings(ds);
applyDisplaySettings(ds);
@@ -1737,15 +1735,13 @@ function openSettings() {
const viewModeEl = $('setting-view-mode');
if (viewModeEl) viewModeEl.value = loadGridViewMode();
// Populate new display toggle checkboxes
// Populate display toggle controls
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;
const activityIndicatorEl = $('setting-activity-indicator');
if (activityIndicatorEl) activityIndicatorEl.value = settings.activityIndicator || 'both';
// Populate Sessions tab / bell sound from display settings
const bellSoundEl = $('setting-bell-sound');
@@ -2270,9 +2266,8 @@ function bindStaticEventListeners() {
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-activity-indicator'), 'change', onDisplaySettingChange);
on($('setting-view-mode'), 'change', function() {
var el = $('setting-view-mode');
if (el) {
+26 -25
View File
@@ -93,6 +93,10 @@
</nav>
<div class="settings-content">
<div class="settings-panel" data-tab="display">
<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 class="settings-field">
<label class="settings-label" for="setting-font-size">Font Size</label>
<select id="setting-font-size" class="settings-select">
@@ -104,6 +108,15 @@
<option value="18">18</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-grid-columns">Grid Columns</label>
<select id="setting-grid-columns" class="settings-select">
<option value="auto" selected>Auto</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-hover-delay">Hover Delay</label>
<select id="setting-hover-delay" class="settings-select">
@@ -115,33 +128,21 @@
</select>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-grid-columns">Grid Columns</label>
<select id="setting-grid-columns" class="settings-select">
<option value="auto" selected>Auto</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<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-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" />
<label class="settings-label" for="setting-activity-indicator">Activity indicator</label>
<select id="setting-activity-indicator" class="settings-select">
<option value="both">Dot + Glow</option>
<option value="glow">Glow only</option>
<option value="dot">Dot only</option>
<option value="none">None</option>
</select>
</div>
</div>
<div class="settings-panel hidden" data-tab="sessions">
@@ -159,10 +160,6 @@
<option value="recent">Recent</option>
</select>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Hidden Sessions</label>
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
</div>
<div class="settings-field">
<label class="settings-label" for="setting-window-size-largest">Auto-set window size (largest)</label>
<input type="checkbox" id="setting-window-size-largest" class="settings-checkbox" />
@@ -182,6 +179,10 @@
<button id="notification-request-btn" class="settings-action-btn">Request Permission</button>
</div>
</div>
<div class="settings-field settings-field--column">
<label class="settings-label">Hidden Sessions</label>
<div id="setting-hidden-sessions" class="settings-checkbox-list"></div>
</div>
</div>
<div class="settings-panel hidden" data-tab="new-session">
<div class="settings-field settings-field--column">
+6 -1
View File
@@ -616,7 +616,7 @@ body {
.tile-bell-dot {
position: absolute;
top: 6px;
bottom: 6px;
right: 6px;
width: 8px;
height: 8px;
@@ -1684,3 +1684,8 @@ body {
#multi-device-fields {
transition: opacity 0.2s ease;
}
/* Constrain device name input width so it doesn't stretch the full settings panel */
#setting-device-name {
max-width: 200px;
}
+129 -11
View File
@@ -3870,12 +3870,12 @@ test('DISPLAY_DEFAULTS includes showDeviceBadges key', () => {
assert.ok(defaultsBody.includes('showDeviceBadges'), 'DISPLAY_DEFAULTS must include showDeviceBadges');
});
test('DISPLAY_DEFAULTS includes showActivityGlow key', () => {
test('DISPLAY_DEFAULTS does not include showActivityGlow (replaced by activityIndicator)', () => {
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');
assert.ok(!defaultsBody.includes('showActivityGlow'), 'DISPLAY_DEFAULTS must NOT include showActivityGlow — replaced by activityIndicator');
});
test('DISPLAY_DEFAULTS includes showHoverPreview key', () => {
@@ -3886,12 +3886,12 @@ test('DISPLAY_DEFAULTS includes showHoverPreview key', () => {
assert.ok(defaultsBody.includes('showHoverPreview'), 'DISPLAY_DEFAULTS must include showHoverPreview');
});
test('DISPLAY_DEFAULTS includes showActivityDot key', () => {
test('DISPLAY_DEFAULTS does not include showActivityDot (replaced by activityIndicator)', () => {
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');
assert.ok(!defaultsBody.includes('showActivityDot'), 'DISPLAY_DEFAULTS must NOT include showActivityDot — replaced by activityIndicator');
});
test('HTML index.html has setting-show-device-badges checkbox', () => {
@@ -3899,9 +3899,9 @@ test('HTML index.html has setting-show-device-badges checkbox', () => {
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', () => {
test('HTML index.html does not have setting-show-activity-glow checkbox (replaced by activity-indicator)', () => {
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');
assert.ok(!source.includes('setting-show-activity-glow'), 'Display panel must NOT have setting-show-activity-glow — replaced by setting-activity-indicator');
});
test('HTML index.html has setting-show-hover-preview checkbox', () => {
@@ -3909,9 +3909,9 @@ test('HTML index.html has setting-show-hover-preview checkbox', () => {
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', () => {
test('HTML index.html does not have setting-show-activity-dot checkbox (replaced by activity-indicator)', () => {
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');
assert.ok(!source.includes('setting-show-activity-dot'), 'Display panel must NOT have setting-show-activity-dot — replaced by setting-activity-indicator');
});
test('CSS style.css has .tile-bell-dot rule', () => {
@@ -3938,15 +3938,133 @@ test('showPreview checks showHoverPreview setting before showing popover', () =>
assert.ok(fnBody.includes('showHoverPreview'), 'showPreview must check showHoverPreview setting before showing popover');
});
test('bindStaticEventListeners binds change events for new display toggle checkboxes', () => {
test('bindStaticEventListeners binds change events for display toggle controls', () => {
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');
assert.ok(fnBody.includes('setting-activity-indicator'), 'must bind setting-activity-indicator');
});
// --- Activity indicator dropdown (replaces showActivityGlow + showActivityDot toggles) ---
test('DISPLAY_DEFAULTS includes activityIndicator key with value both', () => {
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('activityIndicator'), 'DISPLAY_DEFAULTS must include activityIndicator');
assert.ok(defaultsBody.includes("'both'") || defaultsBody.includes('"both"'), "DISPLAY_DEFAULTS activityIndicator default must be 'both'");
});
test('HTML index.html has setting-activity-indicator select element', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
assert.ok(source.includes('setting-activity-indicator'), 'Display panel must have setting-activity-indicator select');
assert.ok(source.includes('value="both"'), 'must have Dot + Glow option with value both');
assert.ok(source.includes('value="glow"'), 'must have Glow only option');
assert.ok(source.includes('value="dot"'), 'must have Dot only option');
assert.ok(source.includes('value="none"'), 'must have None option');
});
test('buildTileHTML shows tile-bell-dot 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('tile-bell-dot'), 'tile-bell-dot must appear when activityIndicator is dot');
_localStorageStore = {};
});
test('buildTileHTML shows tile-bell-dot 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('tile-bell-dot'), 'tile-bell-dot must appear when activityIndicator is both');
_localStorageStore = {};
});
test('buildTileHTML omits tile-bell-dot 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('tile-bell-dot'), 'tile-bell-dot must NOT appear when activityIndicator is none');
_localStorageStore = {};
});
test('buildTileHTML omits tile-bell-dot 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('tile-bell-dot'), 'tile-bell-dot must NOT appear when activityIndicator is glow');
_localStorageStore = {};
});
test('buildTileHTML adds session-tile--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--bell'), 'session-tile--bell must appear when activityIndicator is glow');
_localStorageStore = {};
});
test('buildTileHTML adds session-tile--bell 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--bell'), 'session-tile--bell must appear when activityIndicator is both');
_localStorageStore = {};
});
test('buildTileHTML omits session-tile--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--bell'), 'session-tile--bell must NOT appear when activityIndicator is none');
_localStorageStore = {};
});
test('CSS style.css has #setting-device-name max-width rule', () => {
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
assert.ok(source.includes('#setting-device-name'), 'style.css must have #setting-device-name rule');
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', () => {
const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8');
// Find the .tile-bell-dot rule block
const ruleStart = source.indexOf('.tile-bell-dot {');
assert.ok(ruleStart !== -1, '.tile-bell-dot rule must exist');
const ruleEnd = source.indexOf('}', ruleStart);
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('top: 6px'), '.tile-bell-dot must NOT use top: 6px');
});
test('HTML Display panel device name field appears before font size field', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const displayPanelStart = source.indexOf('<div class="settings-panel" data-tab="display"');
assert.ok(displayPanelStart !== -1, 'display panel must exist');
const nextPanel = source.indexOf('<div class="settings-panel', displayPanelStart + 1);
const displayPanelContent = source.substring(displayPanelStart, nextPanel !== -1 ? nextPanel : displayPanelStart + 3000);
const deviceNameIdx = displayPanelContent.indexOf('setting-device-name');
const fontSizeIdx = displayPanelContent.indexOf('setting-font-size');
assert.ok(deviceNameIdx !== -1, 'device name field must be in display panel');
assert.ok(fontSizeIdx !== -1, 'font size field must be in display panel');
assert.ok(deviceNameIdx < fontSizeIdx, 'device name must appear before font size in display panel');
});
test('HTML Sessions panel hidden sessions field appears after bell sound', () => {
const source = fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8');
const sessionsPanelStart = source.indexOf('<div class="settings-panel hidden" data-tab="sessions"');
assert.ok(sessionsPanelStart !== -1, 'sessions panel must exist');
const nextPanel = source.indexOf('<div class="settings-panel', sessionsPanelStart + 1);
const sessionsPanelContent = source.substring(sessionsPanelStart, nextPanel !== -1 ? nextPanel : sessionsPanelStart + 4000);
const hiddenIdx = sessionsPanelContent.indexOf('setting-hidden-sessions');
const bellSoundIdx = sessionsPanelContent.indexOf('setting-bell-sound');
assert.ok(hiddenIdx !== -1, 'hidden sessions must be in sessions panel');
assert.ok(bellSoundIdx !== -1, 'bell sound must be in sessions panel');
assert.ok(hiddenIdx > bellSoundIdx, 'hidden sessions must appear after bell sound (i.e., near the end)');
});