feat: add initSidebar and toggleSidebar with localStorage persistence
This commit is contained in:
@@ -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.
|
||||
* On mobile, sorts sessions by priority before rendering.
|
||||
@@ -967,6 +1044,8 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
buildTileHTML,
|
||||
buildSidebarHTML,
|
||||
renderSidebar,
|
||||
initSidebar,
|
||||
toggleSidebar,
|
||||
renderGrid,
|
||||
requestNotificationPermission,
|
||||
handleBellTransitions,
|
||||
|
||||
@@ -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
|
||||
globalThis.document = {
|
||||
getElementById: () => null,
|
||||
@@ -17,6 +25,7 @@ globalThis.updatePillBell = () => {};
|
||||
globalThis.window = {
|
||||
addEventListener: () => {},
|
||||
location: { href: '' },
|
||||
innerWidth: 1024,
|
||||
};
|
||||
|
||||
globalThis.Notification = {
|
||||
@@ -1766,3 +1775,154 @@ test('renderSidebar does nothing when view is not fullscreen', () => {
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user