Refactor filter panels and enhance sidebar functionality

- Updated filter panel toggle icons from chevron-up to chevron-down across multiple pages for consistency.
- Added 'collapsed' class to filter content divs to manage visibility state.
- Enhanced library page button for tidying up media items, replacing the trash icon with a custom SVG broom icon.
- Improved CSS styles for sidebar and page header to ensure visibility and proper layout when the sidebar is collapsed.
- Introduced JavaScript functionality to manage sidebar width and state, including theme toggle for light/dark mode.
- Created a new notification drawer template that adapts based on the compact view state.
This commit is contained in:
Matt Batchelder
2026-02-05 09:04:06 -05:00
parent d8f8c0f916
commit 122d098be4
23 changed files with 2447 additions and 190 deletions

View File

@@ -17,7 +17,8 @@
const toggleBtn = document.querySelector('[data-action="toggle-sidebar"]');
const sidebar = document.querySelector('.ots-sidebar');
const closeBtn = document.querySelector('.ots-sidebar-close');
const collapseBtn = document.querySelector('.sidebar-collapse-btn');
const collapseBtn = document.querySelector('.sidebar-collapse-btn-visible');
const expandBtn = document.querySelector('.sidebar-expand-btn');
const body = document.body;
if (!sidebar) return;
@@ -42,6 +43,7 @@
if (isCollapsed) {
sidebar.classList.add('collapsed');
body.classList.add('ots-sidebar-collapsed');
updateSidebarStateClass();
}
collapseBtn.addEventListener('click', function(e) {
@@ -52,6 +54,22 @@
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, nowCollapsed ? 'true' : 'false');
// Update measured sidebar width when collapsed state changes
updateSidebarWidth();
// Recalculate nav offset so items remain below header after collapse
updateSidebarNavOffset();
updateSidebarStateClass();
});
}
if (expandBtn) {
expandBtn.addEventListener('click', function(e) {
e.preventDefault();
sidebar.classList.remove('collapsed');
body.classList.remove('ots-sidebar-collapsed');
localStorage.setItem(STORAGE_KEYS.sidebarCollapsed, 'false');
updateSidebarWidth();
// Recalculate nav offset after expanding
updateSidebarNavOffset();
updateSidebarStateClass();
});
}
@@ -79,18 +97,82 @@
if (!sidebar) return;
// If collapsed, use the known collapsed width; otherwise use measured width
const collapsed = sidebar.classList.contains('collapsed');
const base = collapsed ? 88 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 240;
const padding = 5; // user requested ~5px padding
const value = Math.max(88, Math.round(base + padding));
const base = collapsed ? 64 : sidebar.offsetWidth || sidebar.getBoundingClientRect().width || 256;
const padding = 0;
const value = Math.max(64, Math.round(base + padding));
document.documentElement.style.setProperty('--ots-sidebar-width', `${value}px`);
}
/**
* Measure the sidebar header bottom and set the top padding of the nav list
* so nav items always begin below the header (logo + buttons).
*/
function updateSidebarNavOffset() {
const sidebar = document.querySelector('.ots-sidebar');
if (!sidebar) return;
const header = sidebar.querySelector('.sidebar-header');
const nav = sidebar.querySelector('.sidebar-nav, .ots-sidebar-nav');
if (!nav) return;
// Calculate header bottom relative to the sidebar top (so it works with scrolling)
const sidebarRect = sidebar.getBoundingClientRect();
const headerRect = header ? header.getBoundingClientRect() : null;
let offset = 0;
if (headerRect) {
offset = Math.max(0, Math.ceil(headerRect.bottom - sidebarRect.top));
} else if (header) {
offset = header.offsetHeight || 0;
}
// Add a small gap so nav doesn't touch the header edge
const gap = 8;
const paddingTop = offset > 0 ? offset + gap : '';
// apply as inline style to ensure it overrides static CSS rules
if (paddingTop) {
// Use setProperty with priority so it overrides stylesheet !important rules
try {
nav.style.setProperty('padding-top', `${paddingTop}px`, 'important');
} catch (err) {
// fallback
nav.style.paddingTop = `${paddingTop}px`;
}
} else {
// remove inline override
try {
nav.style.removeProperty('padding-top');
} catch (err) {
nav.style.paddingTop = '';
}
}
}
// simple debounce helper
function debounce(fn, wait) {
let t;
return function () {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, arguments), wait);
};
}
function updateSidebarStateClass() {
const sidebar = document.querySelector('.ots-sidebar');
if (!sidebar) return;
const body = document.body;
const isCollapsed = sidebar.classList.contains('collapsed');
if (!isCollapsed) {
body.classList.add('ots-sidebar-open');
} else {
body.classList.remove('ots-sidebar-open');
}
}
/**
* Initialize sidebar section collapse/expand functionality
*/
function initSidebarSectionToggles() {
const groupToggles = document.querySelectorAll('.sidebar-group-toggle');
syncSidebarActiveStates();
groupToggles.forEach(toggle => {
const group = toggle.closest('.sidebar-group');
const submenu = group ? group.querySelector('.sidebar-submenu') : null;
@@ -112,6 +194,7 @@
group.classList.toggle('is-open', !isOpen);
toggle.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block';
syncSidebarActiveStates();
});
if (caret) {
@@ -141,9 +224,32 @@
group.classList.toggle('is-open', !isOpen);
target.setAttribute('aria-expanded', (!isOpen).toString());
submenu.style.display = isOpen ? 'none' : 'block';
syncSidebarActiveStates();
}, true);
}
function syncSidebarActiveStates() {
const groups = document.querySelectorAll('.sidebar-group');
groups.forEach(group => {
const toggle = group.querySelector('.sidebar-group-toggle');
if (!toggle) return;
const hasActiveChild = Boolean(
group.querySelector('.sidebar-list.active') ||
group.querySelector('.sidebar-list > a.active')
);
toggle.classList.toggle('active', hasActiveChild);
if (hasActiveChild) {
group.classList.add('is-open');
const submenu = group.querySelector('.sidebar-submenu');
if (submenu) submenu.style.display = 'block';
toggle.setAttribute('aria-expanded', 'true');
}
});
}
/**
* Initialize dropdown menus
*/
@@ -462,7 +568,13 @@
initChartSafeguard();
// Set initial sidebar width variable and keep it updated
updateSidebarWidth();
window.addEventListener('resize', updateSidebarWidth);
// Set initial nav offset and keep it updated on resize
updateSidebarNavOffset();
const debouncedUpdateNavOffset = debounce(function() {
updateSidebarNavOffset();
updateSidebarWidth();
}, 120);
window.addEventListener('resize', debouncedUpdateNavOffset);
}
// Wait for DOM to be ready