From 00e0dd815b6c72a58e63972de126a985121a60ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marius=20B=C4=82LTEANU?= Date: Sat, 23 May 2026 18:45:29 +0300 Subject: [PATCH] Improve top menu navigation (#31353): * use nav html tag instead of div * refactor top menu classes for desktop view based on the flyout menu * replace user related links in the top menu bar with a proper user menu * move administration link before user menu as icon only. --- app/assets/javascripts/responsive.js | 13 ++-- app/assets/stylesheets/application.css | 63 ++++++++++++------- app/assets/stylesheets/dropdown.css | 60 ++++++++++++++++++ .../controllers/dropdown_controller.js | 42 +++++++++++++ app/views/layouts/base.html.erb | 43 ++++++++++--- lib/redmine/preparation.rb | 8 ++- 6 files changed, 192 insertions(+), 37 deletions(-) create mode 100644 app/assets/stylesheets/dropdown.css create mode 100644 app/javascript/controllers/dropdown_controller.js diff --git a/app/assets/javascripts/responsive.js b/app/assets/javascripts/responsive.js index da51903ae..6a033fc0a 100644 --- a/app/assets/javascripts/responsive.js +++ b/app/assets/javascripts/responsive.js @@ -55,7 +55,7 @@ function setupFlyout() { $('#main-menu > ul').detach().appendTo('.js-project-menu'); $('#top-menu > ul').detach().appendTo('.js-general-menu'); $('#sidebar > *').detach().appendTo('.js-sidebar'); - $('#account > ul').detach().appendTo('.js-profile-menu'); + $('#account ul').detach().appendTo('.js-profile-menu'); mobileInit = true; desktopInit = false; @@ -65,10 +65,15 @@ function setupFlyout() { var _initDesktopMenu = function() { if(!desktopInit) { - $('.js-project-menu > ul').detach().appendTo('#main-menu'); - $('.js-general-menu > ul').detach().appendTo('#top-menu'); + $('.js-project-menu > ul').detach().prependTo('#main-menu'); + $('.js-general-menu > ul').detach().prependTo('#top-menu'); $('.js-sidebar > *').detach().appendTo('#sidebar'); - $('.js-profile-menu > ul').detach().appendTo('#account'); + + var accountMenuParent = $('#account .dropdown-content'); + if (accountMenuParent.length === 0) { + accountMenuParent = $('#account'); + } + $('.js-profile-menu > ul').detach().appendTo(accountMenuParent); desktopInit = true; mobileInit = false; diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 62a63b212..4fecd967e 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -76,46 +76,63 @@ pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;} position: relative; } -#top-menu { +nav.top-menu { background: #234761; /* no match in Open Color, using hex code */ color: var(--oc-gray-2); font-size: 0.75rem; padding-block: 3px; padding-inline: 20px; + display: flex; + justify-content: space-between; + align-items: center; } -#top-menu ul {margin: 0; padding: 0;} -#top-menu li { - float:inline-start; - list-style-type:none; - margin-block: 0; - margin-inline: 0 12px; + +.general-menu, .profile-menu { + display: flex; + align-items: center; + gap: 8px; +} + +.top-menu__links ul { + margin: 0; padding: 0; - white-space:nowrap; + display: flex; + list-style-type: none; } -#top-menu a { + +.top-menu__links a, .top-menu__links a:link, .top-menu__links a:visited { color: var(--oc-gray-1); margin-inline-end: 0; text-decoration: none; - transition: color 0.18s ease, opacity 0.18s ease; + transition: background-color 0.18s ease, color 0.18s ease; + padding: 4px; } -#top-menu a:hover { - color: var(--oc-white); - text-decoration: underline; - text-underline-offset: 0.18em; + +.top-menu__links a:hover { + color: var(--oc-white); + background-color: rgba(255, 255, 255, 0.12); + transform: scale(1.08); + border-radius: 3px; + text-decoration: none; } -#top-menu #loggedas { - float: inline-end; - margin-inline-end: 20px; - color: var(--oc-gray-5); + +.top-menu__links svg.icon-svg { + stroke: var(--oc-white); } -#top-menu #loggedas a { - color: var(--oc-white); - font-weight: bold; + +#account .dropdown-content .user-info { + padding-block: 10px; + padding-inline: 16px; + color: var(--oc-gray-9); } -#account {float:inline-end;} -#account li:last-of-type {margin-inline-end: 0;} +#account .dropdown-content .user-info .user-name { + font-weight: bold; +} +#account .dropdown-content .logout { + border-block-start: 1px solid var(--oc-gray-2); +} #header { margin: 0; diff --git a/app/assets/stylesheets/dropdown.css b/app/assets/stylesheets/dropdown.css new file mode 100644 index 000000000..50739781f --- /dev/null +++ b/app/assets/stylesheets/dropdown.css @@ -0,0 +1,60 @@ +.dropdown { + position: relative; + display: flex; + align-items: center; +} + +.dropdown-trigger { + display: flex; + align-items: center; + cursor: pointer; + padding-block: 2px; + text-decoration: none; +} + +.dropdown-trigger:hover { + text-decoration: none; +} + +.dropdown-content { + position: absolute; + inset-block-start: 100%; + inset-inline-end: 0; + background-color: var(--oc-white); + border: 1px solid var(--oc-gray-4); + border-radius: 4px; + box-shadow: 0 4px 6px rgba(var(--oc-black-rgb), 0.1); + z-index: 1000; + min-inline-size: 160px; + margin-block-start: 4px; +} + +.dropdown-content ul { + list-style: none; + margin: 0; + padding: 0; +} + +.dropdown-content li { + float: none; + display: block; +} + +.dropdown-content li a, .dropdown-content .dropdown-items a { + display: block; + padding-block: 8px; + padding-inline: 16px; + color: var(--oc-gray-7); + font-weight: normal; + margin: 0; +} + +.dropdown-content li a:hover, .dropdown-content .dropdown-items a:hover { + background-color: var(--oc-gray-1); + color: var(--oc-blue-9); + text-decoration: none; +} + +.dropdown-divider { + border-block-start: 1px solid var(--oc-gray-2); +} diff --git a/app/javascript/controllers/dropdown_controller.js b/app/javascript/controllers/dropdown_controller.js new file mode 100644 index 000000000..9a3ff0aed --- /dev/null +++ b/app/javascript/controllers/dropdown_controller.js @@ -0,0 +1,42 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["content"] + + connect() { + this.closeBinding = this.close.bind(this) + } + + toggle(event) { + event.preventDefault() + event.stopPropagation() + this.contentTarget.classList.toggle("hidden") + + if (!this.contentTarget.classList.contains("hidden")) { + document.addEventListener("click", this.closeBinding) + document.addEventListener("keydown", this.closeBinding) + } else { + document.removeEventListener("click", this.closeBinding) + document.removeEventListener("keydown", this.closeBinding) + } + } + + close(event) { + if (event.type === "keydown" && event.key !== "Escape") { + return + } + + if (event.type === "click" && this.element.contains(event.target)) { + return + } + + this.contentTarget.classList.add("hidden") + document.removeEventListener("click", this.closeBinding) + document.removeEventListener("keydown", this.closeBinding) + } + + disconnect() { + document.removeEventListener("click", this.closeBinding) + document.removeEventListener("keydown", this.closeBinding) + } +} diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 2b9e95cfe..9d7d77db4 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -8,7 +8,7 @@ <%= csrf_meta_tag %> <%= favicon %> -<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'responsive', :media => 'all' %> +<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'dropdown', 'responsive', :media => 'all' %> <%= javascript_importmap_tags %> <%= javascript_heads %> <%= heads_for_theme %> @@ -56,14 +56,43 @@ - -
-
+
+