diff --git a/app/assets/stylesheets/components/navigation.css b/app/assets/stylesheets/components/navigation.css index 11eecc3..b07cade 100644 --- a/app/assets/stylesheets/components/navigation.css +++ b/app/assets/stylesheets/components/navigation.css @@ -47,7 +47,7 @@ display: flex; align-items: center; gap: var(--spacing-3); - flex: 1 1 auto; + flex: 0 0 auto; } .brand-link { @@ -59,6 +59,14 @@ display: inline-flex; align-items: center; gap: var(--spacing-2); + min-width: 0; + flex-shrink: 0; +} + +.brand-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .brand-icon { @@ -66,12 +74,22 @@ width: auto; display: block; object-fit: contain; + flex-shrink: 0; } .tagline { font-size: var(--font-size-xs); font-weight: var(--font-weight-normal); color: var(--color-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 1550px) { + .tagline { + display: none; + } } @media (max-width: 1500px) { @@ -83,12 +101,22 @@ .nav-links { display: flex; gap: var(--spacing-6); + flex: 0 0 auto; } .nav-right { display: flex; align-items: center; gap: var(--spacing-3); + flex: 0 0 auto; +} + +.nav-menu { + display: flex; + align-items: center; + margin-left: auto; + gap: var(--spacing-4); + flex: 0 0 auto; } .nav-auth { @@ -134,6 +162,10 @@ display: none; } +.nav-mobile-star { + display: none; +} + .nav-link-activity i { font-size: 1.05em; } @@ -164,6 +196,67 @@ } } +.nav-overflow-dropdown { + display: none; + position: relative; +} + +.nav-overflow-dropdown.is-visible { + display: inline-flex; +} + +.nav-overflow-toggle { + list-style: none; +} + +.nav-overflow-toggle::marker, +.nav-overflow-toggle::-webkit-details-marker { + display: none; +} + +.nav-overflow-toggle { + padding: var(--spacing-2); + width: auto; + height: auto; +} + +.nav-overflow-menu { + position: absolute; + right: 0; + top: calc(100% + var(--spacing-2)); + background: var(--color-bg-card); + border: var(--border-width) solid var(--color-border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); + padding: var(--spacing-2); + min-width: 220px; + display: flex; + flex-direction: column; + gap: var(--spacing-2); + z-index: 200; +} + +.nav-overflow-menu .nav-link { + width: 100%; + justify-content: space-between; +} + +.nav-overflow-menu form { + width: 100%; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .mobile-nav-dropdown { display: none; position: relative; @@ -233,7 +326,8 @@ } .nav-links, - .nav-right { + .nav-right, + .nav-menu { display: none; } @@ -242,6 +336,14 @@ margin-left: auto; } + .nav-mobile-star { + display: inline-flex; + } + + .nav-mobile-star + .nav-mobile-bell { + margin-left: 0; + } + body.has-sidebar .nav-burger { display: inline-flex; } diff --git a/app/javascript/controllers/nav_overflow_controller.js b/app/javascript/controllers/nav_overflow_controller.js new file mode 100644 index 0000000..d5117fb --- /dev/null +++ b/app/javascript/controllers/nav_overflow_controller.js @@ -0,0 +1,85 @@ +import { Controller } from "@hotwired/stimulus" + +const MOBILE_BREAKPOINT = "(max-width: 900px)" + +export default class extends Controller { + static targets = ["container", "menu", "overflow", "overflowMenu", "item"] + + connect() { + this.mediaQuery = window.matchMedia(MOBILE_BREAKPOINT) + this._resizeHandler = this.layout.bind(this) + window.addEventListener("resize", this._resizeHandler) + this.storePositions() + this.layout() + } + + disconnect() { + window.removeEventListener("resize", this._resizeHandler) + } + + storePositions() { + this.positions = new Map() + this.orderedItems = [...this.itemTargets] + this.orderedItems.forEach((item) => { + const parent = item.parentElement + const index = Array.from(parent.children).indexOf(item) + this.positions.set(item, { parent, index }) + }) + } + + restoreItems() { + const byParent = new Map() + this.positions.forEach((position, item) => { + if (!byParent.has(position.parent)) { + byParent.set(position.parent, []) + } + byParent.get(position.parent).push({ item, index: position.index }) + }) + + byParent.forEach((items, parent) => { + items + .sort((a, b) => a.index - b.index) + .forEach(({ item, index }) => { + const ref = parent.children[index] || null + parent.insertBefore(item, ref) + }) + }) + } + + hideOverflow() { + this.overflowTarget.classList.remove("is-visible") + this.overflowTarget.open = false + } + + showOverflow() { + this.overflowTarget.classList.add("is-visible") + } + + layout() { + if (!this.hasContainerTarget || !this.hasOverflowMenuTarget) return + + this.restoreItems() + this.overflowMenuTarget.innerHTML = "" + this.hideOverflow() + + if (this.mediaQuery.matches) { + return + } + + const fits = this.containerTarget.scrollWidth <= this.containerTarget.clientWidth + if (fits) return + + this.showOverflow() + + for (let i = this.orderedItems.length - 1; i >= 0; i -= 1) { + if (this.containerTarget.scrollWidth <= this.containerTarget.clientWidth) break + const item = this.orderedItems[i] + if (this.overflowMenuTarget.contains(item)) continue + this.overflowMenuTarget.insertBefore(item, this.overflowMenuTarget.firstChild) + } + + if (!this.overflowMenuTarget.children.length) { + this.hideOverflow() + } + } +} diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 21cc599..fd778c9 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -41,8 +41,9 @@ html data-theme="light" - if user_signed_in? && current_user.username.blank? .global-warning span Please set a username in Settings. + - starred_active = controller_name == "topics" && action_name == "index" && params[:filter].to_s == "starred_by_me" nav.main-navigation - .nav-container + .nav-container data-controller="nav-overflow" data-nav-overflow-target="container" .nav-brand - if content_for?(:sidebar) button.nav-burger type="button" aria-label="Toggle sidebar" data-action="click->sidebar#toggleMobile" @@ -56,6 +57,13 @@ html data-theme="light" i.fa-solid.fa-caret-down .mobile-nav-menu data-action="click->sidebar#closeMenuOnNavigate" = link_to "Topics", topics_path, class: "nav-link" + - if user_signed_in? + - icon_class = starred_active ? "fa-solid fa-star" : "fa-regular fa-star" + - link_classes = ["nav-link"] + - link_classes << "is-active" if starred_active + = link_to topics_path(filter: "starred_by_me"), class: link_classes.join(" "), title: "Starred by me", aria: { label: "Starred by me" } do + i class=icon_class aria-hidden="true" + span.sr-only Starred = link_to "Search", topics_path(anchor: "search"), class: "nav-link" = link_to "Statistics", stats_path, class: "nav-link" = link_to "Reports", reports_path, class: "nav-link" @@ -76,37 +84,47 @@ html data-theme="light" span.tagline PostgreSQL Hackers Archive - if user_signed_in? - unread = activity_unread_count + - starred_href = starred_active ? topics_path : topics_path(filter: "starred_by_me") + - starred_title = starred_active ? "All topics" : "Starred by me" + = link_to starred_href, class: "nav-link nav-mobile-star#{' is-active' if starred_active}", title: starred_title, aria: { label: starred_title } do + i class=(starred_active ? "fa-solid fa-star" : "fa-regular fa-star") aria-hidden="true" + span.sr-only Starred = link_to activities_path, class: "nav-link nav-link-activity nav-mobile-bell", title: "Activity" do i.fa-regular.fa-bell - if unread.positive? span.nav-badge = unread - .nav-links - = link_to "Topics", topics_path, class: "nav-link" - - search_link = content_for?(:search_sidebar) ? "#search" : topics_path(anchor: "search") - = link_to "Search", search_link, class: "nav-link" - = link_to "Statistics", stats_path, class: "nav-link" - = link_to "Reports", reports_path, class: "nav-link" - = link_to "Help", help_index_path, class: "nav-link" - .nav-right - button.nav-link.theme-toggle type="button" aria-label="Toggle theme" data-controller="theme" data-action="click->theme#toggle" - i.fas.fa-moon data-theme-target="icon" - span data-theme-target="label" Theme - .nav-auth - - if user_signed_in? - - if current_user&.person&.default_alias - = link_to current_user.person.default_alias.name, person_path(current_user.person.default_alias.email), class: "nav-link nav-user" - - unread = activity_unread_count - = link_to activities_path, class: "nav-link nav-link-activity", title: "Activity" do - i.fa-regular.fa-bell - - if unread.positive? - span.nav-badge = unread - = link_to "Settings", settings_root_path, class: "nav-link" - - if current_admin? - = link_to "Admin", admin_root_path, class: "nav-link" - = button_to "Sign out", session_path, method: :delete, class: "nav-link", form: { style: 'display:inline' }, data: { turbo: false } - - else - = link_to "Sign in", new_session_path, class: "nav-link" - = link_to "Register", new_registration_path, class: "nav-link" + .nav-menu data-nav-overflow-target="menu" + .nav-links + = link_to "Topics", topics_path, class: "nav-link", data: { "nav-overflow-target": "item" } + - search_link = content_for?(:search_sidebar) ? "#search" : topics_path(anchor: "search") + = link_to "Search", search_link, class: "nav-link", data: { "nav-overflow-target": "item" } + = link_to "Statistics", stats_path, class: "nav-link", data: { "nav-overflow-target": "item" } + = link_to "Reports", reports_path, class: "nav-link", data: { "nav-overflow-target": "item" } + = link_to "Help", help_index_path, class: "nav-link", data: { "nav-overflow-target": "item" } + .nav-right + button.nav-link.theme-toggle type="button" aria-label="Toggle theme" data-controller="theme" data-action="click->theme#toggle" data-nav-overflow-target="item" + i.fas.fa-moon data-theme-target="icon" + span data-theme-target="label" Theme + .nav-auth + - if user_signed_in? + - if current_user&.person&.default_alias + = link_to current_user.person.default_alias.name, person_path(current_user.person.default_alias.email), class: "nav-link nav-user", data: { "nav-overflow-target": "item" } + - unread = activity_unread_count + = link_to activities_path, class: "nav-link nav-link-activity", title: "Activity", data: { "nav-overflow-target": "item" } do + i.fa-regular.fa-bell + - if unread.positive? + span.nav-badge = unread + = link_to "Settings", settings_root_path, class: "nav-link", data: { "nav-overflow-target": "item" } + - if current_admin? + = link_to "Admin", admin_root_path, class: "nav-link", data: { "nav-overflow-target": "item" } + = button_to "Sign out", session_path, method: :delete, class: "nav-link", form: { style: 'display:inline', data: { "nav-overflow-target": "item" } }, data: { turbo: false } + - else + = link_to "Sign in", new_session_path, class: "nav-link", data: { "nav-overflow-target": "item" } + = link_to "Register", new_registration_path, class: "nav-link", data: { "nav-overflow-target": "item" } + details.nav-overflow-dropdown data-nav-overflow-target="overflow" + summary.nav-link.nav-overflow-toggle aria-label="More" data-action="click->sidebar#closeMenuOnNavigate" + i.fa-solid.fa-bars + .nav-overflow-menu data-nav-overflow-target="overflowMenu" - if content_for?(:sidebar) .page-layout.with-sidebar data-sidebar-target="layout"