feat: add initSidebar and toggleSidebar with localStorage persistence

This commit is contained in:
Brian Krabach
2026-03-27 17:18:19 -07:00
parent bba862fba3
commit 0c39a79b8a
2 changed files with 239 additions and 0 deletions
+79
View File
@@ -368,6 +368,83 @@ function renderSidebar(sessions, currentSession) {
}); });
} }
const SIDEBAR_KEY = 'muxplex.sidebarOpen';
const SIDEBAR_NARROW_THRESHOLD = 960;
/**
* Initialise sidebar open/closed state on page load.
* Reads muxplex.sidebarOpen from localStorage (JSON.parse with try/catch).
* Defaults to open on wide screens (innerWidth >= 960) when no stored value.
* Applies sidebar--collapsed class accordingly and persists the initial state.
*/
function initSidebar() {
let isOpen;
try {
const stored = localStorage.getItem(SIDEBAR_KEY);
if (stored !== null) {
isOpen = JSON.parse(stored);
} else {
isOpen = window.innerWidth >= SIDEBAR_NARROW_THRESHOLD;
}
} catch (_) {
isOpen = window.innerWidth >= SIDEBAR_NARROW_THRESHOLD;
}
const sidebar = $('sidebar');
if (sidebar) {
if (isOpen) {
sidebar.classList.remove('sidebar--collapsed');
} else {
sidebar.classList.add('sidebar--collapsed');
}
}
// Persist initial state
try {
localStorage.setItem(SIDEBAR_KEY, JSON.stringify(isOpen));
} catch (_) { /* blocked — ok */ }
}
/**
* Toggle the sidebar open/closed state.
* Reads current state from localStorage, inverts it, persists, applies
* sidebar--collapsed class, and updates the collapse button text.
* Button shows when open, when closed.
*/
function toggleSidebar() {
let isOpen;
try {
const stored = localStorage.getItem(SIDEBAR_KEY);
isOpen = stored !== null ? JSON.parse(stored) : true;
} catch (_) {
isOpen = true;
}
// Invert state
isOpen = !isOpen;
// Persist
try {
localStorage.setItem(SIDEBAR_KEY, JSON.stringify(isOpen));
} catch (_) { /* blocked — ok */ }
// Apply class
const sidebar = $('sidebar');
if (sidebar) {
if (isOpen) {
sidebar.classList.remove('sidebar--collapsed');
} else {
sidebar.classList.add('sidebar--collapsed');
}
}
// Update collapse button text ( when open, when closed)
const collapseBtn = $('collapse-btn');
if (collapseBtn) {
collapseBtn.textContent = isOpen ? '\u2039' : '\u203a';
}
}
/** /**
* Render the session grid. Shows empty state when no sessions exist. * Render the session grid. Shows empty state when no sessions exist.
* On mobile, sorts sessions by priority before rendering. * On mobile, sorts sessions by priority before rendering.
@@ -967,6 +1044,8 @@ if (typeof module !== 'undefined' && module.exports) {
buildTileHTML, buildTileHTML,
buildSidebarHTML, buildSidebarHTML,
renderSidebar, renderSidebar,
initSidebar,
toggleSidebar,
renderGrid, renderGrid,
requestNotificationPermission, requestNotificationPermission,
handleBellTransitions, handleBellTransitions,
+160
View File
@@ -1,3 +1,11 @@
// localStorage stub — must be set before importing app.js
let _localStorageStore = {};
globalThis.localStorage = {
getItem: (key) => (Object.prototype.hasOwnProperty.call(_localStorageStore, key) ? _localStorageStore[key] : null),
setItem: (key, value) => { _localStorageStore[key] = String(value); },
removeItem: (key) => { delete _localStorageStore[key]; },
};
// Browser global stubs — must be set before importing app.js // Browser global stubs — must be set before importing app.js
globalThis.document = { globalThis.document = {
getElementById: () => null, getElementById: () => null,
@@ -17,6 +25,7 @@ globalThis.updatePillBell = () => {};
globalThis.window = { globalThis.window = {
addEventListener: () => {}, addEventListener: () => {},
location: { href: '' }, location: { href: '' },
innerWidth: 1024,
}; };
globalThis.Notification = { globalThis.Notification = {
@@ -1766,3 +1775,154 @@ test('renderSidebar does nothing when view is not fullscreen', () => {
globalThis.document.getElementById = origGetById; globalThis.document.getElementById = origGetById;
}); });
// ─── initSidebar ─────────────────────────────────────────────────────────────
test('initSidebar defaults to open (removes sidebar--collapsed) on wide screens when no stored value', () => {
delete _localStorageStore['muxplex.sidebarOpen'];
const origInnerWidth = globalThis.window.innerWidth;
globalThis.window.innerWidth = 1200;
const removedClasses = [];
const addedClasses = [];
const mockSidebar = {
classList: { remove: (c) => removedClasses.push(c), add: (c) => addedClasses.push(c) },
};
const mockCollapseBtn = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar') return mockSidebar;
if (id === 'collapse-btn') return mockCollapseBtn;
return null;
};
app.initSidebar();
assert.ok(removedClasses.includes('sidebar--collapsed'), 'should remove sidebar--collapsed on wide screen');
assert.ok(!addedClasses.includes('sidebar--collapsed'), 'should not add sidebar--collapsed on wide screen');
globalThis.document.getElementById = origGetById;
globalThis.window.innerWidth = origInnerWidth;
});
test('initSidebar defaults to closed (adds sidebar--collapsed) on narrow screens when no stored value', () => {
delete _localStorageStore['muxplex.sidebarOpen'];
const origInnerWidth = globalThis.window.innerWidth;
globalThis.window.innerWidth = 600;
const removedClasses = [];
const addedClasses = [];
const mockSidebar = {
classList: { remove: (c) => removedClasses.push(c), add: (c) => addedClasses.push(c) },
};
const mockCollapseBtn = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar') return mockSidebar;
if (id === 'collapse-btn') return mockCollapseBtn;
return null;
};
app.initSidebar();
assert.ok(addedClasses.includes('sidebar--collapsed'), 'should add sidebar--collapsed on narrow screen');
assert.ok(!removedClasses.includes('sidebar--collapsed'), 'should not remove sidebar--collapsed on narrow screen');
globalThis.document.getElementById = origGetById;
globalThis.window.innerWidth = origInnerWidth;
});
test('initSidebar respects stored value true regardless of screen width — even at 600px removes collapsed class', () => {
_localStorageStore['muxplex.sidebarOpen'] = 'true';
const origInnerWidth = globalThis.window.innerWidth;
globalThis.window.innerWidth = 600;
const removedClasses = [];
const addedClasses = [];
const mockSidebar = {
classList: { remove: (c) => removedClasses.push(c), add: (c) => addedClasses.push(c) },
};
const mockCollapseBtn = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar') return mockSidebar;
if (id === 'collapse-btn') return mockCollapseBtn;
return null;
};
app.initSidebar();
assert.ok(removedClasses.includes('sidebar--collapsed'), 'should remove sidebar--collapsed when stored value is true, even at 600px');
assert.ok(!addedClasses.includes('sidebar--collapsed'), 'should not add sidebar--collapsed when stored value is true');
globalThis.document.getElementById = origGetById;
globalThis.window.innerWidth = origInnerWidth;
});
// ─── toggleSidebar ───────────────────────────────────────────────────────────
test('toggleSidebar persists state to localStorage — from true toggles to false', () => {
_localStorageStore['muxplex.sidebarOpen'] = 'true';
const mockSidebar = {
classList: { remove: () => {}, add: () => {} },
};
const mockCollapseBtn = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar') return mockSidebar;
if (id === 'collapse-btn') return mockCollapseBtn;
return null;
};
app.toggleSidebar();
assert.strictEqual(_localStorageStore['muxplex.sidebarOpen'], 'false', 'should persist false after toggling from true');
globalThis.document.getElementById = origGetById;
});
test('toggleSidebar adds sidebar--collapsed class when closing (from open)', () => {
_localStorageStore['muxplex.sidebarOpen'] = 'true';
const addedClasses = [];
const mockSidebar = {
classList: { remove: () => {}, add: (c) => addedClasses.push(c) },
};
const mockCollapseBtn = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar') return mockSidebar;
if (id === 'collapse-btn') return mockCollapseBtn;
return null;
};
app.toggleSidebar();
assert.ok(addedClasses.includes('sidebar--collapsed'), 'should add sidebar--collapsed class when closing');
globalThis.document.getElementById = origGetById;
});
test('toggleSidebar removes sidebar--collapsed class when opening (from closed) and sets localStorage to true', () => {
_localStorageStore['muxplex.sidebarOpen'] = 'false';
const removedClasses = [];
const mockSidebar = {
classList: { remove: (c) => removedClasses.push(c), add: () => {} },
};
const mockCollapseBtn = { textContent: '' };
const origGetById = globalThis.document.getElementById;
globalThis.document.getElementById = (id) => {
if (id === 'sidebar') return mockSidebar;
if (id === 'collapse-btn') return mockCollapseBtn;
return null;
};
app.toggleSidebar();
assert.ok(removedClasses.includes('sidebar--collapsed'), 'should remove sidebar--collapsed class when opening');
assert.strictEqual(_localStorageStore['muxplex.sidebarOpen'], 'true', 'should persist true after toggling from false');
globalThis.document.getElementById = origGetById;
});