feat(ui): user-configurable sidebar breakpoint width

The threshold above which the channel/DM list shows as a sidebar (vs.
collapsing to a top dropdown) is now user-configurable in
Settings -> Interface -> Layout. Persisted per device in LocalStorage
(key: mc-webui-sidebar-breakpoint, default: 992px, range: 600-2000).

Implementation: replaced hardcoded `@media (min-width: 992px)` with a
`.layout-wide` class on <html>, toggled by JS based on window.innerWidth
vs. the user's breakpoint. An inline script in <head> applies the class
synchronously to prevent layout flash on page load (same pattern as theme).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-05-06 21:35:56 +02:00
parent cf5e95579c
commit 927fc518f0
3 changed files with 112 additions and 19 deletions
+15 -19
View File
@@ -130,14 +130,12 @@ main {
flex-shrink: 0;
}
/* Show sidebar and hide dropdown on wide screens */
@media (min-width: 992px) {
.channel-sidebar {
display: flex;
}
#channelSelectorWrapper {
display: none !important;
}
/* Show sidebar and hide dropdown on wide screens (toggled by JS via .layout-wide on <html>) */
.layout-wide .channel-sidebar {
display: flex;
}
.layout-wide #channelSelectorWrapper {
display: none !important;
}
/* Channel Selector Dropdown (base.html navbar, narrow screens) */
@@ -266,17 +264,15 @@ main {
flex-shrink: 0;
}
/* Show DM sidebar and hide mobile selector on wide screens */
@media (min-width: 992px) {
.dm-sidebar {
display: flex;
}
.dm-mobile-selector {
display: none !important;
}
.dm-desktop-header {
display: block !important;
}
/* Show DM sidebar and hide mobile selector on wide screens (toggled by JS via .layout-wide on <html>) */
.layout-wide .dm-sidebar {
display: flex;
}
.layout-wide .dm-mobile-selector {
display: none !important;
}
.layout-wide .dm-desktop-header {
display: block !important;
}
.dm-desktop-header {
+68
View File
@@ -558,6 +558,10 @@ document.addEventListener('DOMContentLoaded', async function() {
applyItemPlacements();
initializeItemPlacementSettings();
// Sidebar breakpoint: re-apply (covers no-localStorage case) and wire up settings UI + resize listener
applySidebarBreakpoint();
initializeSidebarBreakpointSettings();
// Initialize FAB toggle
initializeFabToggle();
@@ -5382,6 +5386,70 @@ function initializeFabToggle() {
});
}
// =============================================================================
// Sidebar breakpoint (channel/DM list as sidebar vs. dropdown)
// =============================================================================
const SIDEBAR_BREAKPOINT_DEFAULT = 992;
const SIDEBAR_BREAKPOINT_MIN = 600;
const SIDEBAR_BREAKPOINT_MAX = 2000;
const SIDEBAR_BREAKPOINT_KEY = 'mc-webui-sidebar-breakpoint';
function readSidebarBreakpoint() {
const stored = parseInt(localStorage.getItem(SIDEBAR_BREAKPOINT_KEY), 10);
if (isNaN(stored) || stored < SIDEBAR_BREAKPOINT_MIN || stored > SIDEBAR_BREAKPOINT_MAX) {
return SIDEBAR_BREAKPOINT_DEFAULT;
}
return stored;
}
function applySidebarBreakpoint() {
const bp = readSidebarBreakpoint();
document.documentElement.classList.toggle('layout-wide', window.innerWidth >= bp);
}
let _sidebarBreakpointRaf = null;
function onSidebarBreakpointResize() {
if (_sidebarBreakpointRaf) return;
_sidebarBreakpointRaf = requestAnimationFrame(() => {
_sidebarBreakpointRaf = null;
applySidebarBreakpoint();
});
}
function syncSidebarBreakpointUI() {
const input = document.getElementById('settSidebarBreakpoint');
if (input) input.value = readSidebarBreakpoint();
}
function initializeSidebarBreakpointSettings() {
window.addEventListener('resize', onSidebarBreakpointResize);
const input = document.getElementById('settSidebarBreakpoint');
if (input) {
input.addEventListener('input', () => {
const val = parseInt(input.value, 10);
if (isNaN(val) || val < SIDEBAR_BREAKPOINT_MIN || val > SIDEBAR_BREAKPOINT_MAX) return;
localStorage.setItem(SIDEBAR_BREAKPOINT_KEY, String(val));
applySidebarBreakpoint();
});
}
const resetBtn = document.getElementById('settSidebarBreakpointReset');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
localStorage.removeItem(SIDEBAR_BREAKPOINT_KEY);
if (input) input.value = SIDEBAR_BREAKPOINT_DEFAULT;
applySidebarBreakpoint();
});
}
const settingsModal = document.getElementById('settingsModal');
if (settingsModal) {
settingsModal.addEventListener('show.bs.modal', syncSidebarBreakpointUI);
}
}
// =============================================================================
// Chat Filter Functionality
// =============================================================================
+29
View File
@@ -21,6 +21,19 @@
})();
</script>
<!-- Sidebar breakpoint: apply saved preference before CSS loads to prevent layout flash -->
<script>
(function() {
try {
var stored = parseInt(localStorage.getItem('mc-webui-sidebar-breakpoint'), 10);
var bp = (isNaN(stored) || stored < 600 || stored > 2000) ? 992 : stored;
if (window.innerWidth >= bp) {
document.documentElement.classList.add('layout-wide');
}
} catch (e) {}
})();
</script>
<!-- Bootstrap 5 CSS (local) -->
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Bootstrap Icons (local) -->
@@ -666,6 +679,22 @@
</form>
</div>
<div class="tab-pane fade" id="tabSettingsInterface">
<h6 class="text-muted mb-2">Layout</h6>
<p class="text-muted small mb-2">Window width above which the channel/contact list is shown as a sidebar. Below this width it collapses to a dropdown at the top of the screen. Saved per device (browser).</p>
<table class="table table-sm table-borderless mb-3 align-middle">
<tbody>
<tr>
<td class="ps-0">Sidebar breakpoint (px) <span class="badge rounded-pill text-muted" data-bs-toggle="tooltip" title="Default: 992. Range: 600-2000."><i class="bi bi-info-circle"></i></span></td>
<td class="pe-0" style="width:10rem">
<div class="d-flex gap-1">
<input type="number" class="form-control form-control-sm" id="settSidebarBreakpoint" min="600" max="2000" step="1" value="992">
<button type="button" class="btn btn-outline-secondary btn-sm" id="settSidebarBreakpointReset" title="Reset to default (992)"><i class="bi bi-arrow-counterclockwise"></i></button>
</div>
</td>
</tr>
</tbody>
</table>
<hr>
<form id="uiSettingsForm">
<h6 class="text-muted mb-2">Notifications</h6>
<p class="text-muted small mb-2">Controls the small toasts shown after actions (e.g. "Advert Sent", errors).</p>