From 5cbda52d47a9b04eb4905b4480d18c8253b1d8d8 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 14 May 2026 21:45:45 -0700 Subject: [PATCH 1/2] Add DocsSubNavbar component with section-based navigation - Create DocsSubNavbar.astro: renders section tabs from sidebar data using nav-overflow - Create _subnavbar.scss: sticky section nav styles - Update BaseLayout.astro: render DocsSubNavbar in sticky header for docs layout - Update docs.scss: import subnavbar styles Co-authored-by: Cursor --- site/src/components/DocsSubNavbar.astro | 56 +++++++++++++++++++++++++ site/src/layouts/BaseLayout.astro | 6 +++ site/src/scss/_subnavbar.scss | 19 +++++++++ site/src/scss/docs.scss | 1 + 4 files changed, 82 insertions(+) create mode 100644 site/src/components/DocsSubNavbar.astro create mode 100644 site/src/scss/_subnavbar.scss diff --git a/site/src/components/DocsSubNavbar.astro b/site/src/components/DocsSubNavbar.astro new file mode 100644 index 000000000000..6d2c3159ba1e --- /dev/null +++ b/site/src/components/DocsSubNavbar.astro @@ -0,0 +1,56 @@ +--- +import { getData } from '@libs/data' +import { getSlug } from '@libs/utils' +import { getVersionedDocsPath } from '@libs/path' + +interface Props { + currentSlug: string +} + +const { currentSlug } = Astro.props +const sidebar = getData('sidebar') + +// Build ordered, deduplicated list of sections with their first-group info +const seen = new Set() +const sections: { section: string; href: string }[] = [] + +for (const group of sidebar) { + if (!group.section || seen.has(group.section)) continue + seen.add(group.section) + + // Find the first linkable page in this group + const firstItem = group.pages?.find((p) => p.title && !p.href) + const href = firstItem?.title + ? getVersionedDocsPath(`${getSlug(group.title)}/${getSlug(firstItem.title)}/`) + : getVersionedDocsPath(`${getSlug(group.title)}/`) + + sections.push({ section: group.section, href }) +} + +// Detect active section from the current slug (e.g. "getting-started/install" -> "getting-started") +const currentDir = currentSlug?.split('/')[0] ?? '' +const activeSection = + sidebar.find((g) => getSlug(g.title) === currentDir)?.section ?? null +--- + + diff --git a/site/src/layouts/BaseLayout.astro b/site/src/layouts/BaseLayout.astro index d12421e5509c..9a62c061e6d3 100644 --- a/site/src/layouts/BaseLayout.astro +++ b/site/src/layouts/BaseLayout.astro @@ -7,6 +7,7 @@ import Header from '@components/header/Header.astro' import Scripts from '@components/Scripts.astro' import Footer from '@components/footer/Footer.astro' import { stripMarkdown } from '@libs/utils' +import DocsSubNavbar from '@components/DocsSubNavbar.astro' // The following props can be directly passed to the base layout component from any page or layout extending it, // e.g. . @@ -42,6 +43,10 @@ const thumbnail = frontmatter?.thumbnail ? `img/${frontmatter.thumbnail}` : 'bra const bodyProps = overrides?.body ?? {} const mainProps = overrides?.main ?? {} + +const docsCurrentSlug = layout === 'docs' + ? Astro.url.pathname.replace(/^\/docs\/[^/]+\//, '').replace(/\/$/, '') + : '' --- @@ -52,6 +57,7 @@ const mainProps = overrides?.main ?? {} { diff --git a/site/src/scss/_subnavbar.scss b/site/src/scss/_subnavbar.scss new file mode 100644 index 000000000000..fad65388f4b9 --- /dev/null +++ b/site/src/scss/_subnavbar.scss @@ -0,0 +1,19 @@ +@use "../../../scss/config" as *; +@use "../../../scss/layout/breakpoints" as *; + +@layer custom { + .bd-subnavbar { + background-color: var(--bs-body-bg); + + .nav { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + + .nav-link { + white-space: nowrap; + padding-block: .5rem; + } + } +} diff --git a/site/src/scss/docs.scss b/site/src/scss/docs.scss index 80d92d21be9a..6b13765356c2 100644 --- a/site/src/scss/docs.scss +++ b/site/src/scss/docs.scss @@ -26,6 +26,7 @@ // Load docs components // @use "variables"; @use "navbar"; +@use "subnavbar"; @use "masthead"; @use "ads"; @use "content"; From 07eef2976492b77b14aedd68d05672bb5fea0db7 Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 14 May 2026 22:30:06 -0700 Subject: [PATCH 2/2] phew, receover some changes from before --- site/data/sidebar.yml | 2 + site/src/components/DocsSidebar.astro | 65 +++++++++++++++++++------ site/src/components/DocsSubNavbar.astro | 4 +- 3 files changed, 53 insertions(+), 18 deletions(-) diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index b3036aa12068..cfb3e1764994 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -13,6 +13,8 @@ - title: JavaScript - title: Accessibility # - title: Community + - title: Examples + href: examples/ - title: Guides section: Guides diff --git a/site/src/components/DocsSidebar.astro b/site/src/components/DocsSidebar.astro index 4da2d840b4c5..8cb4751fccda 100644 --- a/site/src/components/DocsSidebar.astro +++ b/site/src/components/DocsSidebar.astro @@ -6,33 +6,66 @@ import { getSlug } from '@libs/utils' import NewBadge from '@components/NewBadge.astro' const sidebar = getData('sidebar') + +// Derive active section from the current slug +const { slug } = Astro.params +const currentDir = (slug as string)?.split('/')[0] ?? '' +const activeSection = sidebar.find((g) => getSlug(g.title) === currentDir)?.section + +// Only show groups that belong to the active section; fall back to all groups +const visibleGroups = activeSection ? sidebar.filter((g) => g.section === activeSection) : sidebar + +// Skip group headings when there's only one group in the section +const skipGroupHeading = visibleGroups.filter((g) => g.pages).length === 1 ---