From 80659c713bbada55f6e5f7828111d96c3b93680e Mon Sep 17 00:00:00 2001 From: UNITRONIX <36471318+UNITRONIX@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:39:43 +0200 Subject: [PATCH] fix(ui): keep Settings tab bar sticky and flush on mobile Bleed cancels content padding so the bar sits at the scroll top; sticky stays on under 900px/mobile instead of scrolling away; active tab scrolls into view on click/hash. Refs #396 Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow) --- CHANGELOG.md | 3 ++ web-nodejs/public/css/mobile-shell.css | 6 ++-- web-nodejs/public/css/pages.css | 41 ++++++++++++++++---------- web-nodejs/public/css/ux35.css | 9 ++++-- web-nodejs/public/js/settings.js | 5 +++- web-nodejs/views/settings.ejs | 4 ++- 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2cfcaf..3e9dc289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ ### Added - **Custom download portal product label (#386):** Agent Generator advanced branding can override or hide the product type line (`download.product_*`) on the public download page without rebuilding installers. +### Fixed +- **Settings tab bar sticky + mobile (#396):** Settings horizontal tabs stay flush under the top of the content scroll area (bleed cancels content padding), keep sticky on narrow/mobile instead of scrolling away, and scroll the active tab into view when switching or opening a hash link. Ships via panel update. Verify: Settings → scroll a long form → tab bar flush at top; ≤900px / mobile → tabs remain visible while scrolling; `/settings#updates` brings Updates into the horizontal track. + ### Changed - _(none yet)_ diff --git a/web-nodejs/public/css/mobile-shell.css b/web-nodejs/public/css/mobile-shell.css index da81d951..5ea90be1 100644 --- a/web-nodejs/public/css/mobile-shell.css +++ b/web-nodejs/public/css/mobile-shell.css @@ -468,7 +468,8 @@ body.ux35-page .mobile-bottom-nav { -webkit-overflow-scrolling: touch; } - body.app-page.is-mobile-shell .settings-shell-nav, + /* Settings: sticky outer stays; horizontal scroll on the track only */ + body.app-page.is-mobile-shell .settings-shell-nav-track, body.app-page.is-mobile-shell .settings-tabs, body.app-page.is-mobile-shell .server-mgmt-tabs { display: flex; @@ -478,10 +479,9 @@ body.ux35-page .mobile-bottom-nav { scrollbar-width: none; gap: var(--space-xs); padding-bottom: var(--space-xs); - position: static; } - body.app-page.is-mobile-shell .settings-shell-nav::-webkit-scrollbar, + body.app-page.is-mobile-shell .settings-shell-nav-track::-webkit-scrollbar, body.app-page.is-mobile-shell .settings-tabs::-webkit-scrollbar, body.app-page.is-mobile-shell .server-mgmt-tabs::-webkit-scrollbar { display: none; diff --git a/web-nodejs/public/css/pages.css b/web-nodejs/public/css/pages.css index a505d572..371630df 100644 --- a/web-nodejs/public/css/pages.css +++ b/web-nodejs/public/css/pages.css @@ -3285,20 +3285,21 @@ } .settings-shell-nav { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - gap: 4px; + --settings-nav-bleed: 0px; + --settings-nav-bleed-x: 0px; + display: block; padding: 0; margin: 0; border: none; border-bottom: 1px solid var(--border-primary); border-radius: 0; - overflow-x: auto; - overscroll-behavior-x: contain; - -webkit-overflow-scrolling: touch; + overflow: visible; position: sticky; - top: 0; + top: calc(-1 * var(--settings-nav-bleed)); + padding-top: var(--settings-nav-bleed); + margin-top: calc(-1 * var(--settings-nav-bleed)); + margin-inline: calc(-1 * var(--settings-nav-bleed-x)); + padding-inline: var(--settings-nav-bleed-x); z-index: 2; /* Keep labels readable while scrolling long forms / branding wallpaper */ backdrop-filter: blur(8px); @@ -3306,6 +3307,23 @@ background-color: color-mix(in srgb, var(--bg-primary) 88%, transparent); } +/* Classic layout: cancel .main-content padding so sticky bar is flush */ +.app-page:not(.ux35-page) .settings-shell-nav { + --settings-nav-bleed: var(--space-lg); + --settings-nav-bleed-x: var(--space-lg); +} + +/* Horizontal scroll lives on the track — not the sticky outer (avoids sticky breakage) */ +.settings-shell-nav-track { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 4px; + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; +} + .settings-shell-nav .settings-shell-tab, .settings-shell-nav .settings-tab { display: flex; @@ -3768,13 +3786,6 @@ } @media (max-width: 900px) { - .settings-shell-nav { - position: static; - backdrop-filter: none; - -webkit-backdrop-filter: none; - background-color: transparent; - } - .settings-row { grid-template-columns: 1fr; gap: var(--space-sm); diff --git a/web-nodejs/public/css/ux35.css b/web-nodejs/public/css/ux35.css index db361461..5dbcfc1d 100644 --- a/web-nodejs/public/css/ux35.css +++ b/web-nodejs/public/css/ux35.css @@ -424,10 +424,11 @@ body.app-page.ux35-page { margin-bottom: var(--ux35-card-gap); } -/* Settings shell: horizontal tabs readable over branding wallpaper */ +/* Settings shell: horizontal tabs readable over branding wallpaper; bleed cancels content padding */ .ux35-page .settings-shell-nav { + --settings-nav-bleed: var(--ux35-content-padding-lg); + --settings-nav-bleed-x: var(--ux35-content-padding-lg); position: sticky; - top: 0; z-index: 2; background-color: color-mix(in srgb, var(--bg-primary) 92%, transparent); } @@ -579,6 +580,10 @@ body.ux35-is-resizing { padding: var(--ux35-content-padding); padding-bottom: calc(var(--ux35-content-padding) + 72px); } + .ux35-page .settings-shell-nav { + --settings-nav-bleed: var(--ux35-content-padding); + --settings-nav-bleed-x: var(--ux35-content-padding); + } .ux35-sidebar { width: min(86vw, 280px); min-width: min(86vw, 280px); diff --git a/web-nodejs/public/js/settings.js b/web-nodejs/public/js/settings.js index 055cad5e..e8fac569 100644 --- a/web-nodejs/public/js/settings.js +++ b/web-nodejs/public/js/settings.js @@ -142,6 +142,9 @@ const target = document.getElementById('tab-' + tab.dataset.tab); if (target) target.classList.add('active'); window.history.replaceState(null, '', '#' + tab.dataset.tab); + if (typeof tab.scrollIntoView === 'function') { + tab.scrollIntoView({ inline: 'nearest', block: 'nearest' }); + } applySettingsSearchFilter(); onSettingsTabChanged(tab.dataset.tab); }); @@ -150,7 +153,7 @@ const hash = window.location.hash.replace('#', ''); if (['general', 'branding', 'server', 'backup', 'updates', 'auth', 'advanced', 'email'].includes(hash)) { const tabName = hash === 'server' ? 'general' : hash; - const tab = document.querySelector(`[data-tab="${tabName}"]`); + const tab = document.querySelector(`.settings-shell-tab[data-tab="${tabName}"], .settings-tab[data-tab="${tabName}"]`); if (tab) tab.click(); } } diff --git a/web-nodejs/views/settings.ejs b/web-nodejs/views/settings.ejs index 28b35d5f..b7b27e1f 100644 --- a/web-nodejs/views/settings.ejs +++ b/web-nodejs/views/settings.ejs @@ -11,7 +11,8 @@
-