From c4bfa6a41bcdd55427568a42462095daaa858cf2 Mon Sep 17 00:00:00 2001 From: Stanislav Zhuk Date: Thu, 30 Jul 2026 11:44:15 +0300 Subject: [PATCH] layout: measure the header nav rather than collapsing it at a breakpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## The Issue Follow-up to the review on #704. The header nav and the blog post title both switched to their narrow layouts at `lg` (1024px). Browser zoom shrinks the viewport in CSS pixels, so at 110-125% a 1200px window falls under that and the nav collapsed wholesale under the menu button. `w-2/3` on the nav's right-hand wrapper capped its contents at two thirds of the container, which is why the threshold had to be that high. The menu behind that button was a `div` only the script could reveal, so with JavaScript off there was no way to reach the nav links below 1024px at all. The byline's edit link was anchored to the full 72rem `site-container` instead of the text, landing about 26rem past the end of it on posts with no feature image. ## How This PR Solves The Issue - Nav links come from one array rendered into both the bar and the menu. A script measures them and drops links from the end until the rest fit, moving each one into the menu. The button shows only when something is hidden. Without the script the markup still falls back to `hidden lg:flex`. - The menu is a `details`/`summary`, so it opens with JavaScript off and the script keeps no open/close code of its own. The separate close button is gone; the summary toggles. - Nothing in the bar may shrink, or the measurements come back compressed and report room that isn't there. The menu's width stays reserved while it's hidden, so the result can't oscillate. - `.title-grid` puts the blog title beside its feature image from 52rem, with the image column at `clamp(16rem, 36%, 25rem)` so it shrinks instead of stacking. - The byline row is capped at `--reading-measure`. ## Manual Testing Instructions Preview: https://pr-REPLACE_ME_WITH_PR_NUMBER.ddev-com-fork-previews.pages.dev/ 1. Drag the window from 1400px to 320px: links should leave the bar one at a time and appear in the menu, with the logo never compressed. 2. At 1200px, zoom to 110%, 125% and 150%: as many links as fit at each. 3. Compare `/blog/release-v1-25-3/` against a post with no feature image, at 1400px, 900px and 700px. 4. With JavaScript off: all links above 1024px, and below it the button opens the menu. ## Release/Deployment Notes None. 🤖 Developed with assistance from [Claude Code](https://claude.ai/code) Co-authored-by: Claude Opus 5 --- src/components/Header.astro | 264 +++++++++++++++++------------------ src/components/Heading.astro | 11 +- src/styles/global.css | 51 ++++++- 3 files changed, 177 insertions(+), 149 deletions(-) diff --git a/src/components/Header.astro b/src/components/Header.astro index 9d1c0e72..a9677875 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -3,54 +3,100 @@ * Global site header. */ import { GITHUB_URL, DOCUMENTATION_URL } from "../const" -import { - MagnifyingGlassIcon, - Bars2Icon, - XMarkIcon, -} from "@heroicons/react/24/solid/index.js" +import { MagnifyingGlassIcon, Bars2Icon } from "@heroicons/react/24/solid/index.js" import SponsorsBanner from "./SponsorsBanner.astro" import ThemeToggle from "./ThemeToggle.astro" + +// Links leave the bar from the end as it runs out of room, so this is also +// least-important-last. +const navItems = [ + { label: "Get Started", href: "/get-started" }, + { label: "Download", href: "/download" }, + { label: "Docs", href: DOCUMENTATION_URL, external: true }, + { label: "Blog", href: "/blog" }, + { label: "GitHub", href: GITHUB_URL, external: true }, + { label: "❤️ Sponsor DDEV", href: "/sponsor" }, +] --- @@ -60,10 +106,11 @@ import ThemeToggle from "./ThemeToggle.astro" diff --git a/src/components/Heading.astro b/src/components/Heading.astro index 6c8ad0d0..f26bb1a3 100644 --- a/src/components/Heading.astro +++ b/src/components/Heading.astro @@ -34,12 +34,7 @@ const hasByline = Boolean(author || date || readTime || editUrl) ---

{title}

@@ -51,7 +46,7 @@ const hasByline = Boolean(author || date || readTime || editUrl) carried its margins, pushing those pages' content down. */} { hasByline && ( -
+
{ author && (
@@ -125,7 +120,7 @@ const hasByline = Boolean(author || date || readTime || editUrl) { hasAside && ( -
+
) diff --git a/src/styles/global.css b/src/styles/global.css index 79288f28..e65084eb 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -45,6 +45,28 @@ @apply mt-16 lg:col-start-3 lg:mt-0 lg:pl-12; } +/* A page title with something beside it, such as a blog post's feature image + * (see Heading.astro). The image column is fluid, so the two can sit side by + * side well before the title has to give up its own width. */ +.title-grid { + @apply grid items-start gap-8; +} + +.title-aside { + @apply mt-8; +} + +@media (min-width: 52rem) { + .title-grid { + grid-template-columns: 1fr clamp(16rem, 36%, 25rem); + gap: 3rem; + } + + .title-aside { + margin-top: 0; + } +} + /* A block of links or a small call to action in a page's sidebar column. */ .sidebar-card { @apply rounded-lg border border-gray-200 bg-gray-50 p-5 dark:border-slate-600 dark:bg-slate-800; @@ -136,14 +158,37 @@ mark { @apply dark:bg-[blue]; } +/* The panel `details` opens, positioned against the page rather than the + * summary, so it hangs from the top right corner as it always has. */ .menu { - @apply hidden absolute top-0 right-0 max-w-full bg-white px-8 py-4 z-30 text-center shadow-2xl rounded-lg m-3 dark:bg-slate-800 dark:text-white; + @apply absolute top-0 right-0 max-w-full bg-white px-8 py-4 z-30 text-center shadow-2xl rounded-lg m-3 dark:bg-slate-800 dark:text-white; +} - &.open { - @apply block; +/* The summary is the menu button, so it loses the disclosure triangle. */ +.nav-summary { + @apply flex cursor-pointer p-1; + list-style: none; + + &::-webkit-details-marker { + display: none; } } +/* Set once Header.astro's script can measure what fits, replacing the + * `hidden lg:flex` fallback in the markup. Two classes beat one, no + * `!important` needed. */ +.nav-measured .nav-links { + display: flex; +} + +.nav-measured .nav-menu { + display: block; +} + +.nav-measured [data-nav-hidden] { + display: none; +} + input[type="radio"]:checked + label { @apply font-bold border-blue-300 shadow border; }