Skip to content

Commit 927fc51

Browse files
MarekWoclaude
andcommitted
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>
1 parent cf5e955 commit 927fc51

3 files changed

Lines changed: 112 additions & 19 deletions

File tree

app/static/css/style.css

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,12 @@ main {
130130
flex-shrink: 0;
131131
}
132132

133-
/* Show sidebar and hide dropdown on wide screens */
134-
@media (min-width: 992px) {
135-
.channel-sidebar {
136-
display: flex;
137-
}
138-
#channelSelectorWrapper {
139-
display: none !important;
140-
}
133+
/* Show sidebar and hide dropdown on wide screens (toggled by JS via .layout-wide on <html>) */
134+
.layout-wide .channel-sidebar {
135+
display: flex;
136+
}
137+
.layout-wide #channelSelectorWrapper {
138+
display: none !important;
141139
}
142140

143141
/* Channel Selector Dropdown (base.html navbar, narrow screens) */
@@ -266,17 +264,15 @@ main {
266264
flex-shrink: 0;
267265
}
268266

269-
/* Show DM sidebar and hide mobile selector on wide screens */
270-
@media (min-width: 992px) {
271-
.dm-sidebar {
272-
display: flex;
273-
}
274-
.dm-mobile-selector {
275-
display: none !important;
276-
}
277-
.dm-desktop-header {
278-
display: block !important;
279-
}
267+
/* Show DM sidebar and hide mobile selector on wide screens (toggled by JS via .layout-wide on <html>) */
268+
.layout-wide .dm-sidebar {
269+
display: flex;
270+
}
271+
.layout-wide .dm-mobile-selector {
272+
display: none !important;
273+
}
274+
.layout-wide .dm-desktop-header {
275+
display: block !important;
280276
}
281277

282278
.dm-desktop-header {

app/static/js/app.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,10 @@ document.addEventListener('DOMContentLoaded', async function() {
558558
applyItemPlacements();
559559
initializeItemPlacementSettings();
560560

561+
// Sidebar breakpoint: re-apply (covers no-localStorage case) and wire up settings UI + resize listener
562+
applySidebarBreakpoint();
563+
initializeSidebarBreakpointSettings();
564+
561565
// Initialize FAB toggle
562566
initializeFabToggle();
563567

@@ -5382,6 +5386,70 @@ function initializeFabToggle() {
53825386
});
53835387
}
53845388

5389+
// =============================================================================
5390+
// Sidebar breakpoint (channel/DM list as sidebar vs. dropdown)
5391+
// =============================================================================
5392+
5393+
const SIDEBAR_BREAKPOINT_DEFAULT = 992;
5394+
const SIDEBAR_BREAKPOINT_MIN = 600;
5395+
const SIDEBAR_BREAKPOINT_MAX = 2000;
5396+
const SIDEBAR_BREAKPOINT_KEY = 'mc-webui-sidebar-breakpoint';
5397+
5398+
function readSidebarBreakpoint() {
5399+
const stored = parseInt(localStorage.getItem(SIDEBAR_BREAKPOINT_KEY), 10);
5400+
if (isNaN(stored) || stored < SIDEBAR_BREAKPOINT_MIN || stored > SIDEBAR_BREAKPOINT_MAX) {
5401+
return SIDEBAR_BREAKPOINT_DEFAULT;
5402+
}
5403+
return stored;
5404+
}
5405+
5406+
function applySidebarBreakpoint() {
5407+
const bp = readSidebarBreakpoint();
5408+
document.documentElement.classList.toggle('layout-wide', window.innerWidth >= bp);
5409+
}
5410+
5411+
let _sidebarBreakpointRaf = null;
5412+
function onSidebarBreakpointResize() {
5413+
if (_sidebarBreakpointRaf) return;
5414+
_sidebarBreakpointRaf = requestAnimationFrame(() => {
5415+
_sidebarBreakpointRaf = null;
5416+
applySidebarBreakpoint();
5417+
});
5418+
}
5419+
5420+
function syncSidebarBreakpointUI() {
5421+
const input = document.getElementById('settSidebarBreakpoint');
5422+
if (input) input.value = readSidebarBreakpoint();
5423+
}
5424+
5425+
function initializeSidebarBreakpointSettings() {
5426+
window.addEventListener('resize', onSidebarBreakpointResize);
5427+
5428+
const input = document.getElementById('settSidebarBreakpoint');
5429+
if (input) {
5430+
input.addEventListener('input', () => {
5431+
const val = parseInt(input.value, 10);
5432+
if (isNaN(val) || val < SIDEBAR_BREAKPOINT_MIN || val > SIDEBAR_BREAKPOINT_MAX) return;
5433+
localStorage.setItem(SIDEBAR_BREAKPOINT_KEY, String(val));
5434+
applySidebarBreakpoint();
5435+
});
5436+
}
5437+
5438+
const resetBtn = document.getElementById('settSidebarBreakpointReset');
5439+
if (resetBtn) {
5440+
resetBtn.addEventListener('click', () => {
5441+
localStorage.removeItem(SIDEBAR_BREAKPOINT_KEY);
5442+
if (input) input.value = SIDEBAR_BREAKPOINT_DEFAULT;
5443+
applySidebarBreakpoint();
5444+
});
5445+
}
5446+
5447+
const settingsModal = document.getElementById('settingsModal');
5448+
if (settingsModal) {
5449+
settingsModal.addEventListener('show.bs.modal', syncSidebarBreakpointUI);
5450+
}
5451+
}
5452+
53855453
// =============================================================================
53865454
// Chat Filter Functionality
53875455
// =============================================================================

app/templates/base.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@
2121
})();
2222
</script>
2323

24+
<!-- Sidebar breakpoint: apply saved preference before CSS loads to prevent layout flash -->
25+
<script>
26+
(function() {
27+
try {
28+
var stored = parseInt(localStorage.getItem('mc-webui-sidebar-breakpoint'), 10);
29+
var bp = (isNaN(stored) || stored < 600 || stored > 2000) ? 992 : stored;
30+
if (window.innerWidth >= bp) {
31+
document.documentElement.classList.add('layout-wide');
32+
}
33+
} catch (e) {}
34+
})();
35+
</script>
36+
2437
<!-- Bootstrap 5 CSS (local) -->
2538
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
2639
<!-- Bootstrap Icons (local) -->
@@ -666,6 +679,22 @@ <h6 class="text-muted mb-2">Route popup</h6>
666679
</form>
667680
</div>
668681
<div class="tab-pane fade" id="tabSettingsInterface">
682+
<h6 class="text-muted mb-2">Layout</h6>
683+
<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>
684+
<table class="table table-sm table-borderless mb-3 align-middle">
685+
<tbody>
686+
<tr>
687+
<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>
688+
<td class="pe-0" style="width:10rem">
689+
<div class="d-flex gap-1">
690+
<input type="number" class="form-control form-control-sm" id="settSidebarBreakpoint" min="600" max="2000" step="1" value="992">
691+
<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>
692+
</div>
693+
</td>
694+
</tr>
695+
</tbody>
696+
</table>
697+
<hr>
669698
<form id="uiSettingsForm">
670699
<h6 class="text-muted mb-2">Notifications</h6>
671700
<p class="text-muted small mb-2">Controls the small toasts shown after actions (e.g. "Advert Sent", errors).</p>

0 commit comments

Comments
 (0)