Modified - Added the persistent sticky side-menu preference and the finalized sidebar behavior for /user/settings and /-/admin.
This commit is contained in:
@@ -572,3 +572,9 @@ Project Change ID[date-time] - application-version - Type - Summary:
|
||||
114 - [2026-05-07 09:30:56] - v1.27.0-dev-105-gb3a7692ce9 - Type: Modified - Moved the persistent footer outside the page scroll area.
|
||||
- 1 - I modified `web_src/css/base.css` so pages with `show-persistent-footer` stop scrolling the whole `body` and instead scroll the `.full.height` content container, keeping the footer in its own fixed viewport slot.
|
||||
- 2 - I modified `web_src/css/home.css` so the persistent footer no longer overlays the page with `position: fixed`, but stays permanently visible as a non-scrolling sibling below the main content area.
|
||||
|
||||
115 - [2026-05-07 09:54:43] - v1.27.0-dev-106-g61a9e4bd06 - Type: Modified - Added the persistent sticky side-menu preference and the finalized sidebar behavior for `/user/settings` and `/-/admin`.
|
||||
- 1 - I modified `models/user/setting_options.go`, `routers/common/pagetmpl.go`, `templates/base/head.tmpl`, `routers/web/user/setting/profile.go`, `routers/web/web.go`, `templates/user/settings/appearance.tmpl`, `options/locale/locale_en-US.json`, `options/locale/locale_ro-RO.json`, and `models/user/setting_test.go` to add the persisted `ui.sticky_side_menus` preference, expose it to templates as `ShowStickySideMenus`, save it from `/user/settings/appearance`, document it in the UI, and cover it with a focused test.
|
||||
- 2 - I modified `templates/user/settings/actions_general.tmpl` so `/user/settings/actions/general` passes the expected `pageClass` and participates in the same sticky-sidebar behavior as the other settings subpages.
|
||||
- 3 - I modified `web_src/css/modules/flexcontainer.css` so, when the preference is enabled, the shared left menus on `/user/settings` and `/-/admin` use the current sticky layout: sticky positioning on the inner menu, `top: var(--page-spacing)`, footer-aware `max-height` handling for both persistent and non-persistent footer modes, hidden desktop scrollbars, green overflow hints, local menu scrolling, and the current `/user/settings` item padding.
|
||||
- 4 - I modified `web_src/js/features/common-page.ts` so the sticky side menus update their overflow-hint classes dynamically as their scroll position, size, or expanded sections change.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
7a2d7f9780aab8a6ed828973079ce7c58da5f69d
|
||||
80b62fc2b4904d0194cd619dc7ce5e24b11a47c5
|
||||
|
||||
@@ -15,6 +15,8 @@ const (
|
||||
SettingsKeyHiddenCommentTypes = "issue.hidden_comment_types"
|
||||
// SettingsKeyPersistentFooter is the setting key for whether the footer remains visible in the viewport
|
||||
SettingsKeyPersistentFooter = "ui.persistent_footer"
|
||||
// SettingsKeyStickySideMenus is the setting key for whether settings/admin side menus remain visible while scrolling
|
||||
SettingsKeyStickySideMenus = "ui.sticky_side_menus"
|
||||
// SettingsKeyDiffWhitespaceBehavior is the setting key for whitespace behavior of diff
|
||||
SettingsKeyDiffWhitespaceBehavior = "diff.whitespace_behaviour"
|
||||
// SettingsKeyShowOutdatedComments is the setting key whether or not to show outdated comments in PRs
|
||||
|
||||
@@ -71,3 +71,16 @@ func TestPersistentFooterSetting(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, enabled)
|
||||
}
|
||||
|
||||
func TestStickySideMenusSetting(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
assert.NoError(t, user_model.SetUserSetting(t.Context(), 99, user_model.SettingsKeyStickySideMenus, "true"))
|
||||
|
||||
val, err := user_model.GetUserSetting(t.Context(), 99, user_model.SettingsKeyStickySideMenus, "false")
|
||||
assert.NoError(t, err)
|
||||
|
||||
enabled, err := strconv.ParseBool(val)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, enabled)
|
||||
}
|
||||
|
||||
@@ -807,6 +807,9 @@
|
||||
"settings.footer": "Footer",
|
||||
"settings.footer_desc": "Choose whether the site footer should remain visible at the bottom of the viewport.",
|
||||
"settings.show_persistent_footer": "Keep the footer visible on screen",
|
||||
"settings.side_menus": "Side menus",
|
||||
"settings.side_menus_desc": "Choose whether the left menus in user settings and the admin area should remain visible while the page content scrolls.",
|
||||
"settings.show_sticky_side_menus": "Keep the left menus visible while scrolling",
|
||||
"settings.privacy": "Privacy",
|
||||
"settings.keep_activity_private": "Hide Activity from profile page",
|
||||
"settings.keep_activity_private_popup": "Makes the activity visible only for you and the admins",
|
||||
|
||||
@@ -807,6 +807,9 @@
|
||||
"settings.footer": "Footer",
|
||||
"settings.footer_desc": "Alege dacă footer-ul site-ului trebuie să rămână vizibil în partea de jos a ferestrei.",
|
||||
"settings.show_persistent_footer": "Păstrează footer-ul vizibil pe ecran",
|
||||
"settings.side_menus": "Meniuri laterale",
|
||||
"settings.side_menus_desc": "Alege dacă meniurile din stânga din setările utilizatorului și din zona de administrare trebuie să rămână vizibile în timp ce conținutul paginii se derulează.",
|
||||
"settings.show_sticky_side_menus": "Păstrează meniurile din stânga vizibile la derulare",
|
||||
"settings.privacy": "Confidențialitate",
|
||||
"settings.keep_activity_private": "Ascunde activitatea din pagina de profil",
|
||||
"settings.keep_activity_private_popup": "Activitatea va fi vizibilă doar pentru tine și administratori",
|
||||
|
||||
@@ -85,6 +85,7 @@ func PageGlobalData(ctx *context.Context) {
|
||||
ctx.Data["PageGlobalData"] = data
|
||||
|
||||
showPersistentFooter := false
|
||||
showStickySideMenus := false
|
||||
if ctx.Doer != nil {
|
||||
val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyPersistentFooter, "false")
|
||||
if err != nil {
|
||||
@@ -93,8 +94,16 @@ func PageGlobalData(ctx *context.Context) {
|
||||
showPersistentFooter, _ = strconv.ParseBool(val)
|
||||
ctx.SetSiteCookie(middleware.CookiePersistentFooter, strconv.FormatBool(showPersistentFooter), 0)
|
||||
}
|
||||
|
||||
val, err = user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyStickySideMenus, "false")
|
||||
if err != nil {
|
||||
log.Error("GetUserSetting[%s] for user %d: %v", user_model.SettingsKeyStickySideMenus, ctx.Doer.ID, err)
|
||||
} else {
|
||||
showStickySideMenus, _ = strconv.ParseBool(val)
|
||||
}
|
||||
} else {
|
||||
showPersistentFooter, _ = strconv.ParseBool(middleware.GetSiteCookie(ctx.Req, middleware.CookiePersistentFooter))
|
||||
}
|
||||
ctx.Data["ShowPersistentFooter"] = showPersistentFooter
|
||||
ctx.Data["ShowStickySideMenus"] = showStickySideMenus
|
||||
}
|
||||
|
||||
@@ -443,3 +443,17 @@ func UpdatePersistentFooter(ctx *context.Context) {
|
||||
ctx.Flash.Success(ctx.Tr("settings.saved_successfully"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
|
||||
}
|
||||
|
||||
// UpdateStickySideMenus updates whether settings/admin side menus remain visible while scrolling.
|
||||
func UpdateStickySideMenus(ctx *context.Context) {
|
||||
showStickySideMenus := ctx.FormBool("show_sticky_side_menus")
|
||||
|
||||
if err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyStickySideMenus, strconv.FormatBool(showStickySideMenus)); err != nil {
|
||||
ctx.ServerError("SetUserSetting", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("User settings updated: %s", ctx.Doer.Name)
|
||||
ctx.Flash.Success(ctx.Tr("settings.saved_successfully"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
|
||||
}
|
||||
|
||||
@@ -636,6 +636,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Group("/appearance", func() {
|
||||
m.Get("", user_setting.Appearance)
|
||||
m.Post("/footer", user_setting.UpdatePersistentFooter)
|
||||
m.Post("/sticky_side_menus", user_setting.UpdateStickySideMenus)
|
||||
m.Post("/language", web.Bind(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang)
|
||||
m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments)
|
||||
m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{{template "base/head_script" .}}
|
||||
{{template "custom/header" .}}
|
||||
</head>
|
||||
<body{{if .ShowPersistentFooter}} class="show-persistent-footer"{{end}} hx-swap="outerHTML" hx-push-url="false">
|
||||
<body{{if or .ShowPersistentFooter .ShowStickySideMenus}} class="{{if .ShowPersistentFooter}}show-persistent-footer{{end}}{{if and .ShowPersistentFooter .ShowStickySideMenus}} {{end}}{{if .ShowStickySideMenus}}show-sticky-side-menus{{end}}"{{end}} hx-swap="outerHTML" hx-push-url="false">
|
||||
{{template "custom/body_outer_pre" .}}
|
||||
|
||||
<div class="full height">
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{{template "user/settings/layout_head" (dict "ctxData" .)}}
|
||||
{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings actions")}}
|
||||
{{template "shared/actions/owner_general_settings" .}}
|
||||
{{template "user/settings/layout_footer" .}}
|
||||
|
||||
@@ -56,6 +56,27 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Side menus -->
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.side_menus"}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.Link}}/sticky_side_menus" method="post">
|
||||
<p class="help">
|
||||
{{ctx.Locale.Tr "settings.side_menus_desc"}}
|
||||
</p>
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<input name="show_sticky_side_menus" type="checkbox" {{if .ShowStickySideMenus}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "settings.show_sticky_side_menus"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Language -->
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "settings.language"}}
|
||||
|
||||
@@ -21,6 +21,62 @@
|
||||
min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu {
|
||||
position: sticky;
|
||||
top: var(--page-spacing);
|
||||
max-height: calc(100dvh - var(--page-spacing) - var(--page-space-bottom) - 24px);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
body.show-persistent-footer.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu {
|
||||
max-height: calc(100dvh - var(--page-spacing) - 96px - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
body.show-persistent-footer.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu {
|
||||
max-height: calc(100dvh - var(--page-spacing) - 96px - env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu > .item,
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu > details.toggleable-item > summary {
|
||||
padding: 0.9em 1.14285714em;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu::-webkit-scrollbar,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu.has-scroll-hint-top,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu.has-scroll-hint-top {
|
||||
box-shadow: inset 0 1px 0 var(--color-green), inset 0 16px 14px -10px var(--color-green-badge-hover-bg);
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu.has-scroll-hint-bottom,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu.has-scroll-hint-bottom {
|
||||
box-shadow: inset 0 -1px 0 var(--color-green), inset 0 -16px 14px -10px var(--color-green-badge-hover-bg);
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu.has-scroll-hint-top.has-scroll-hint-bottom,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu.has-scroll-hint-top.has-scroll-hint-bottom {
|
||||
box-shadow: inset 0 1px 0 var(--color-green), inset 0 -1px 0 var(--color-green), inset 0 16px 14px -10px var(--color-green-badge-hover-bg), inset 0 -16px 14px -10px var(--color-green-badge-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.flex-container {
|
||||
flex-direction: column;
|
||||
@@ -30,4 +86,11 @@
|
||||
order: -1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
body.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu,
|
||||
body.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,38 @@ function initFooterThemeSelector() {
|
||||
});
|
||||
}
|
||||
|
||||
function initStickySideMenuIndicators() {
|
||||
const menus = queryElems<HTMLDivElement>(document, [
|
||||
'.show-sticky-side-menus .page-content.user.settings .flex-container-nav > .ui.vertical.menu',
|
||||
'.show-sticky-side-menus .page-content.admin .flex-container-nav > .ui.vertical.menu',
|
||||
].join(', '));
|
||||
|
||||
for (const menu of menus) {
|
||||
const updateIndicators = () => {
|
||||
const maxScrollTop = menu.scrollHeight - menu.clientHeight;
|
||||
const isScrollable = maxScrollTop > 1;
|
||||
menu.classList.toggle('has-scroll-hint-top', isScrollable && menu.scrollTop > 1);
|
||||
menu.classList.toggle('has-scroll-hint-bottom', isScrollable && menu.scrollTop < maxScrollTop - 1);
|
||||
};
|
||||
const scheduleUpdate = () => window.requestAnimationFrame(updateIndicators);
|
||||
|
||||
menu.addEventListener('scroll', updateIndicators);
|
||||
window.addEventListener('resize', scheduleUpdate);
|
||||
new ResizeObserver(scheduleUpdate).observe(menu);
|
||||
new MutationObserver(scheduleUpdate).observe(menu, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['open'],
|
||||
});
|
||||
scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
export function initCommmPageComponents() {
|
||||
initHeadNavbarContentToggle();
|
||||
initFooterLanguageMenu();
|
||||
initFooterThemeSelector();
|
||||
initStickySideMenuIndicators();
|
||||
}
|
||||
|
||||
export function initGlobalDropdown() {
|
||||
|
||||
Reference in New Issue
Block a user