From 9b9fccf06aeeaaee23bacc12ec79d2bd8cfff7cd Mon Sep 17 00:00:00 2001 From: Jeff Merrick Date: Mon, 18 May 2026 15:23:29 -0500 Subject: [PATCH] Rebuild docs top nav to match the marketing header Replaces the legacy docs-top-nav.html with a data-driven implementation that mirrors header/nav.html: Tailwind utility classes, the shared github-star-button partial, and CSS-only auth-aware Sign in / Dashboard swap. - Nav contents now live in data/docs_nav.yaml (logo dropdown, items, CTAs) - Logo dropdown handler moved out of packages.ts into header-nav.ts - Drops theme/src/scss/docs/_docs-top-nav.scss and the top-nav-user-toggle.html partial; both are superseded by the new markup - header_nav.yaml: books -> book-open for icon consistency - Pre-paint inline script in head.html sets from the pulumi_web_user_info cookie, with unlayered CSS rules driving the [data-nav-loggedout]/[data-nav-dashboard] swap. Replaces the post-hydration JS toggle so the swap happens before first paint and contributes nothing to CLS for signed-in users. The docs nav intentionally does not stick to the top of the viewport and does not ship a mobile hamburger sheet -- below lg, the docs sidebar is the primary nav surface. Co-Authored-By: Claude Opus 4.7 --- data/docs_nav.yaml | 43 +++++ data/header_nav.yaml | 2 +- layouts/partials/docs-top-nav.html | 151 +++++++++--------- layouts/partials/head.html | 11 ++ layouts/partials/header.html | 4 - layouts/partials/header/nav-mobile-sheet.html | 1 - layouts/partials/header/nav.html | 2 - layouts/partials/top-nav-user-toggle.html | 4 - theme/src/scss/docs/_docs-top-nav.scss | 137 ---------------- theme/src/scss/main.scss | 11 +- theme/src/ts/header-nav.ts | 44 +++-- theme/src/ts/packages.ts | 34 ---- 12 files changed, 171 insertions(+), 273 deletions(-) create mode 100644 data/docs_nav.yaml delete mode 100644 layouts/partials/top-nav-user-toggle.html delete mode 100644 theme/src/scss/docs/_docs-top-nav.scss diff --git a/data/docs_nav.yaml b/data/docs_nav.yaml new file mode 100644 index 000000000000..fafdfaf8ee73 --- /dev/null +++ b/data/docs_nav.yaml @@ -0,0 +1,43 @@ +# Docs/tutorials top nav. Consumed by layouts/partials/docs-top-nav.html. +# `showAt` (sm | md | lg) hides the item below that breakpoint. + +logoMenu: + - { label: Pulumi home, href: / } + - { label: Pricing, href: /pricing/ } + - { label: Blog, href: /blog/ } + - { label: Events & workshops, href: /events/ } + +items: + - label: Docs + href: /docs/ + icon: book-open + track: header-docs + showAt: lg + - label: Registry + href: /registry/ + icon: package + track: header-registry + showAt: lg + - label: Pulumi Neo + href: /product/neo/ + icon: custom/pulumi-neo + iconWeight: fill + track: header-ai + showAt: lg + - label: Slack + href: https://slack.pulumi.com/ + icon: brand/slack + track: header-slack + showAt: md + +cta: + github: + track: header-github-stars + showAt: sm + contact: { label: Contact us, href: /contact/, track: header-contact, showAt: sm } + signIn: { label: Sign in, href: https://app.pulumi.com/signin, track: header-console } + dashboard: { label: Dashboard, href: https://app.pulumi.com, track: header-dashboard } + getStarted: + label: Get started + href: /docs/get-started/ + track: get-started-practitioner-nav diff --git a/data/header_nav.yaml b/data/header_nav.yaml index 306d7cf3ee9b..dd64acec93c3 100644 --- a/data/header_nav.yaml +++ b/data/header_nav.yaml @@ -55,7 +55,7 @@ items: - label: Documentation href: /docs/ description: Complete guides and API references - icon: books + icon: book-open track: header-engineers-docs - label: Registry href: /registry/ diff --git a/layouts/partials/docs-top-nav.html b/layouts/partials/docs-top-nav.html index ed2ef62200cc..6073315852a0 100644 --- a/layouts/partials/docs-top-nav.html +++ b/layouts/partials/docs-top-nav.html @@ -1,73 +1,80 @@ - + +
+ {{ range $item := $nav.items }} + + {{ partial "icon.html" (dict "name" $item.icon "weight" (default "regular" $item.iconWeight)) }} + {{ $item.label }} + + {{ end }} + + {{ partial "github-star-button.html" (dict + "variant" "header" + "track" $nav.cta.github.track + "extraClass" (index $showAt $nav.cta.github.showAt)) }} + + + {{ $nav.cta.contact.label }} + + + {{ $nav.cta.signIn.label }} + + + {{ $nav.cta.dashboard.label }} + + + {{ $nav.cta.getStarted.label }} + +
+ + + + + diff --git a/layouts/partials/head.html b/layouts/partials/head.html index aa630c51cb20..09fc0995aebc 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -390,4 +390,15 @@ {{ if and (eq .Type "blog") .IsPage .Params.updated }} {{ end }} + + {{- /* Sets before first paint when the auth + cookie is present, so the header's Sign-in -> Dashboard swap doesn't + cause layout shift for logged-in users. Paired with CSS in main.scss. */ -}} + diff --git a/layouts/partials/header.html b/layouts/partials/header.html index c92465138c48..55041e2d81df 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -1,9 +1,5 @@ {{ if or (hasPrefix .RelPermalink "/docs/") (hasPrefix .RelPermalink "/tutorials/") }} {{ partial "docs-top-nav" . }} -
- - {{ .Scratch.Set "mode" .Section }} -
{{ else }} {{ partial "header/nav.html" . }} {{ end }} diff --git a/layouts/partials/header/nav-mobile-sheet.html b/layouts/partials/header/nav-mobile-sheet.html index ac399f216686..9dc6a19dc6f9 100644 --- a/layouts/partials/header/nav-mobile-sheet.html +++ b/layouts/partials/header/nav-mobile-sheet.html @@ -91,7 +91,6 @@ diff --git a/layouts/partials/header/nav.html b/layouts/partials/header/nav.html index 90a2360707f5..1b6ef779b905 100644 --- a/layouts/partials/header/nav.html +++ b/layouts/partials/header/nav.html @@ -33,7 +33,6 @@ @@ -65,7 +64,6 @@ diff --git a/layouts/partials/top-nav-user-toggle.html b/layouts/partials/top-nav-user-toggle.html deleted file mode 100644 index 4cf12de0f83e..000000000000 --- a/layouts/partials/top-nav-user-toggle.html +++ /dev/null @@ -1,4 +0,0 @@ - - Pulumi Cloud - Sign In - diff --git a/theme/src/scss/docs/_docs-top-nav.scss b/theme/src/scss/docs/_docs-top-nav.scss deleted file mode 100644 index 8c0cc05cb325..000000000000 --- a/theme/src/scss/docs/_docs-top-nav.scss +++ /dev/null @@ -1,137 +0,0 @@ -nav.actually-docs { - padding: 0; - - div.top-nav-container { - margin: 8px 12px; - display: flex; - justify-content: space-between; - align-items: center; - flex-direction: row; - - div.logo { - display: flex; - align-items: center; - min-height: 38px; - } - - div.logo-hamburger { - display: flex; - justify-content: space-between; - } - - div.logo-parent img { - height: 24px; - } - - div.logo-get-started { - display: flex; - align-items: center; - gap: 18px; - - div.get-started a.get-started-header-button { - margin-left: 0; - } - - @media (min-width: 640px){ - gap: 36px; - } - } - - div.logo-get-started, - div.logo-parent { - @media (min-width: 640px){ - width: auto; - } - } - - div.get-started { - a.get-started-header-button { - width: auto; - height: auto; - display: flex; - box-shadow: none; - color: #fff; - padding: 8px 16px; - border: 1px solid var(--color-violet-primary); - border-radius: 4px; - background: var(--color-violet-primary); - - &:hover { - color: #131314; - background: transparent; - } - } - } - - ul.logo-nav-menu { - position: absolute; - top: 46px; - width: auto; - margin-left: 0; - padding: 0; - font-family: Inter; - z-index: 100; - - li { - font-family: Inter; - font-size: 16px; - } - } - - div.nav-items ul li:not(:last-child) { - margin-right: 24px; - } - - div.nav-items ul { - display: flex; - - li i - li div.icon { - margin-right: 4px; - } - - li a { - display: flex; - align-items: center; - gap: 4px; - } - - li.github-widget, - li.slack, - li.docs, - li.registry, - li.ai, - li.contact, - div.get-started { - display: none; - } - - li.github-widget, - li.slack { - @media (min-width: 900px){ - display: flex; - } - } - - li.docs, - li.registry { - @media (min-width: 768px) { - display: flex - } - } - - li.ai, - li.contact { - @media (min-width: 475px) { - display: flex - } - } - - div.get-started { - @media (min-width: 450px) { - display: flex; - } - } - } - } -} diff --git a/theme/src/scss/main.scss b/theme/src/scss/main.scss index 58ce34897631..6a700e8825c3 100644 --- a/theme/src/scss/main.scss +++ b/theme/src/scss/main.scss @@ -229,7 +229,6 @@ $sitenav-offset: calc($sitenav-height + 16px); @import "copy-button"; @import "docs/continuous-delivery"; @import "docs/docs-main"; - @import "docs/docs-top-nav"; @import "docs/main-nav"; @import "docs/docs-home"; @import "customer-logo"; @@ -465,3 +464,13 @@ $sitenav-offset: calc($sitenav-height + 16px); @import "consent-banner"; +// Auth-aware header swap. The inline script in head.html adds .is-signed-in +// to when the auth cookie is present, before first paint. These rules +// live unlayered so they win against the buttons' display utilities (which +// live in @layer utilities/components). When signed-in, the dashboard rule +// is dropped and the buttons' own `hidden nav-desktop:inline-flex` style +// rules decide visibility per breakpoint — so we don't end up showing both +// the desktop and mobile dashboard variants at once. +html:not(.is-signed-in) [data-nav-dashboard] { display: none; } +html.is-signed-in [data-nav-loggedout] { display: none; } + diff --git a/theme/src/ts/header-nav.ts b/theme/src/ts/header-nav.ts index ddeac57c05a0..377f19b44ba8 100644 --- a/theme/src/ts/header-nav.ts +++ b/theme/src/ts/header-nav.ts @@ -472,22 +472,32 @@ }); }); - // ---------- Auth-aware sign-in / dashboard toggle ---------- - try { - const cookies: Record = {}; - document.cookie.split(';').forEach(c => { - const eq = c.indexOf('='); - if (eq > -1) cookies[c.slice(0, eq).trim()] = c.slice(eq + 1).trim(); + // ---------- Docs nav: Pulumi-logo dropdown ---------- + const logoTrigger = document.querySelector('[data-logo-nav-trigger]'); + const logoMenu = document.querySelector('[data-logo-nav-menu]'); + if (logoTrigger && logoMenu) { + const setLogoOpen = (open: boolean): void => { + logoTrigger.setAttribute('aria-expanded', open ? 'true' : 'false'); + if (open) logoMenu.removeAttribute('hidden'); + else logoMenu.setAttribute('hidden', ''); + }; + logoTrigger.addEventListener('click', e => { + e.stopPropagation(); + setLogoOpen(logoTrigger.getAttribute('aria-expanded') !== 'true'); }); - const userCookie = cookies['pulumi_web_user_info'] ?? 'j:{}'; - const userInfo = JSON.parse(decodeURIComponent(userCookie).slice(2)); - if (userInfo?.userId) { - document.querySelectorAll('[data-nav-loggedout]').forEach(el => { - el.style.display = 'none'; - }); - document.querySelectorAll('[data-nav-dashboard]').forEach(el => { - el.style.removeProperty('display'); - }); - } - } catch (e) {} + document.addEventListener('click', e => { + if (logoTrigger.getAttribute('aria-expanded') !== 'true') return; + const t = e.target as Node; + if (!logoTrigger.contains(t) && !logoMenu.contains(t)) setLogoOpen(false); + }); + document.addEventListener('keydown', e => { + if (e.key === 'Escape' && logoTrigger.getAttribute('aria-expanded') === 'true') { + setLogoOpen(false); + logoTrigger.focus(); + } + }); + } + + // The .is-signed-in class on is set pre-paint by the inline script in + // head.html — see CSS rules in main.scss. Nothing to do here at hydration. })(); diff --git a/theme/src/ts/packages.ts b/theme/src/ts/packages.ts index c5e3d47c1b02..921c098eebf6 100644 --- a/theme/src/ts/packages.ts +++ b/theme/src/ts/packages.ts @@ -126,37 +126,3 @@ document.querySelector(".section-registry")?.addEventListener("packageSearch", ( const allCount = document.querySelectorAll(".all-packages .package:not(.hidden)").length; document.querySelectorAll(".all-count").forEach(el => el.textContent = String(allCount)); }); - - -document.addEventListener("DOMContentLoaded", function () { - const logoNavMenuButton = document.querySelector(".logo-nav-button") as HTMLElement; - const bgMask = document.querySelector(".logo-nav-bg-mask") as HTMLElement; - const logoNavMenu = document.getElementById("logo-nav-menu"); - - if (!logoNavMenuButton || !logoNavMenu) return; - - function toggleMenu() { - logoNavMenu.classList.toggle("hidden"); - const navMenuVisible = !logoNavMenu.classList.contains("hidden"); - logoNavMenuButton.setAttribute("aria-expanded", `${navMenuVisible}`); - document.querySelectorAll(".logo-nav-button .mobile-menu-toggle-icon").forEach(el => el.classList.toggle("hidden")); - bgMask?.classList.toggle("hidden"); - } - - logoNavMenuButton.addEventListener("click", toggleMenu); - bgMask?.addEventListener("click", toggleMenu); - - document.addEventListener("click", function (event) { - const target = event.target as HTMLElement; - if (!target.closest(".logo-nav-button") && !target.closest("#logo-nav-menu") && !logoNavMenu.classList.contains("hidden")) { - toggleMenu(); - } - }); - - document.addEventListener("scroll", function () { - const PRACTITIONER_NAV_HEIGHT = 53; - if (window.scrollY > PRACTITIONER_NAV_HEIGHT && !logoNavMenu.classList.contains("hidden")) { - toggleMenu(); - } - }); -});