From 927fc518f01e5722dd589d93c745e49115eb09b8 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Wed, 6 May 2026 21:35:56 +0200 Subject: [PATCH] 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 , toggled by JS based on window.innerWidth vs. the user's breakpoint. An inline script in applies the class synchronously to prevent layout flash on page load (same pattern as theme). Co-Authored-By: Claude Opus 4.7 --- app/static/css/style.css | 34 +++++++++----------- app/static/js/app.js | 68 ++++++++++++++++++++++++++++++++++++++++ app/templates/base.html | 29 +++++++++++++++++ 3 files changed, 112 insertions(+), 19 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index a792efb..6f8f53d 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -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 ) */ +.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 ) */ +.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 { diff --git a/app/static/js/app.js b/app/static/js/app.js index 7c1e14b..1c1df54 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -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 // ============================================================================= diff --git a/app/templates/base.html b/app/templates/base.html index 20da22a..4a540b1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -21,6 +21,19 @@ })(); + + + @@ -666,6 +679,22 @@
+
Layout
+

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).

+ + + + + + + +
Sidebar breakpoint (px) +
+ + +
+
+
Notifications

Controls the small toasts shown after actions (e.g. "Advert Sent", errors).