From df21f0bb6d45c2b4e46b0a2a6b5b273e903a20b4 Mon Sep 17 00:00:00 2001 From: Charan Date: Wed, 22 Jul 2026 22:38:38 +0530 Subject: [PATCH] Unify secondary top nav styling and add mobile scroll hints. Consolidate tabs, anchors, and navbar links into SecondaryTopNav with consistent active underlines, flush header borders, and pathname-aware highlighting for blog/changelog routes. Co-authored-by: Cursor --- .../inkform-docs/app/[[...slug]]/page.tsx | 3 +- .../inkform-docs/app/blog/[slug]/page.tsx | 2 +- examples/inkform-docs/app/blog/page.tsx | 2 +- examples/inkform-docs/app/changelog/page.tsx | 2 +- examples/inkform-docs/app/theme.css | 5 +- examples/inkform-docs/components/top-bar.tsx | 101 +++-------------- .../markdown-docs/app/[[...slug]]/page.tsx | 3 +- .../markdown-docs/app/blog/[slug]/page.tsx | 2 +- examples/markdown-docs/app/blog/page.tsx | 2 +- examples/markdown-docs/app/changelog/page.tsx | 2 +- examples/markdown-docs/app/theme.css | 5 +- examples/markdown-docs/components/top-bar.tsx | 107 ++---------------- .../pokeapi-docs/app/[[...slug]]/page.tsx | 3 +- .../app/api-reference/[[...slug]]/page.tsx | 6 +- .../pokeapi-docs/app/blog/[slug]/page.tsx | 2 +- examples/pokeapi-docs/app/blog/page.tsx | 2 +- examples/pokeapi-docs/app/changelog/page.tsx | 2 +- examples/pokeapi-docs/app/theme.css | 5 +- examples/pokeapi-docs/components/top-bar.tsx | 107 ++---------------- packages/framework/package.json | 3 +- packages/framework/src/scrollable-top-nav.tsx | 74 ++++++++++++ packages/framework/src/secondary-top-nav.tsx | 95 ++++++++++++++++ packages/framework/src/styles/layout.css | 89 ++++++++++++--- templates/canopy/app/[[...slug]]/page.tsx | 3 +- .../app/api-reference/[[...slug]]/page.tsx | 6 +- templates/canopy/app/blog/[slug]/page.tsx | 2 +- templates/canopy/app/blog/page.tsx | 2 +- templates/canopy/app/changelog/page.tsx | 2 +- templates/canopy/app/theme.css | 5 +- templates/canopy/components/top-bar.tsx | 107 ++---------------- templates/galley/app/[[...slug]]/page.tsx | 3 +- .../app/api-reference/[[...slug]]/page.tsx | 6 +- templates/galley/app/blog/[slug]/page.tsx | 2 +- templates/galley/app/blog/page.tsx | 2 +- templates/galley/app/changelog/page.tsx | 2 +- templates/galley/app/theme.css | 5 +- templates/galley/components/top-bar.tsx | 107 ++---------------- templates/shadcn/app/[[...slug]]/page.tsx | 3 +- .../app/api-reference/[[...slug]]/page.tsx | 6 +- templates/shadcn/app/blog/[slug]/page.tsx | 2 +- templates/shadcn/app/blog/page.tsx | 2 +- templates/shadcn/app/changelog/page.tsx | 2 +- templates/shadcn/app/theme.css | 5 +- templates/shadcn/components/top-bar.tsx | 107 ++---------------- 44 files changed, 387 insertions(+), 618 deletions(-) create mode 100644 packages/framework/src/scrollable-top-nav.tsx create mode 100644 packages/framework/src/secondary-top-nav.tsx diff --git a/examples/inkform-docs/app/[[...slug]]/page.tsx b/examples/inkform-docs/app/[[...slug]]/page.tsx index ff89b53..77d58bc 100644 --- a/examples/inkform-docs/app/[[...slug]]/page.tsx +++ b/examples/inkform-docs/app/[[...slug]]/page.tsx @@ -43,7 +43,8 @@ export default async function Page({ params }: { params: Promise<{ slug?: string const headings = extractHeadings(page.content); const { prev, next } = docNeighbours(config, ref.slug); const sidebar = sidebarForDoc(config, tab, ref.slug, renderIcon); - const topBar = buildTopBar(withContentNavLinks(config), tab.tab); + const pathname = (slug ?? []).length === 0 ? '/' : `/${(slug ?? []).join('/')}`; + const topBar = buildTopBar(withContentNavLinks(config), tab.tab, pathname); return ( diff --git a/examples/inkform-docs/app/blog/page.tsx b/examples/inkform-docs/app/blog/page.tsx index 7a04806..0d96026 100644 --- a/examples/inkform-docs/app/blog/page.tsx +++ b/examples/inkform-docs/app/blog/page.tsx @@ -9,7 +9,7 @@ export const metadata = { title: 'Blog' }; export default function BlogIndexPage() { const config = loadDocsConfig(); const posts = loadBlogPosts(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/blog') : null; return ( diff --git a/examples/inkform-docs/app/changelog/page.tsx b/examples/inkform-docs/app/changelog/page.tsx index 890f6a9..936434f 100644 --- a/examples/inkform-docs/app/changelog/page.tsx +++ b/examples/inkform-docs/app/changelog/page.tsx @@ -10,7 +10,7 @@ export const metadata = { title: 'Changelog' }; export default function ChangelogPage() { const config = loadDocsConfig(); const entries = loadChangelogEntries(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/changelog') : null; return ( diff --git a/examples/inkform-docs/app/theme.css b/examples/inkform-docs/app/theme.css index f4c28f2..163f887 100644 --- a/examples/inkform-docs/app/theme.css +++ b/examples/inkform-docs/app/theme.css @@ -124,7 +124,7 @@ html.dark { row-gap: 0.625rem; align-items: center; padding-top: 0.875rem; - padding-bottom: 0.75rem; + padding-bottom: 0; } .fw-shell-header-start { @@ -199,7 +199,8 @@ html.dark { } .fw-topnav-tab { - padding: 0.4rem 0.0625rem 0.6rem; + padding: 0.5rem 0.0625rem 0.75rem; + margin-bottom: -1px; border: none; border-bottom: 2px solid transparent; border-radius: 0; diff --git a/examples/inkform-docs/components/top-bar.tsx b/examples/inkform-docs/components/top-bar.tsx index 13d637f..8b31275 100644 --- a/examples/inkform-docs/components/top-bar.tsx +++ b/examples/inkform-docs/components/top-bar.tsx @@ -2,9 +2,8 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { AskAi } from '@inkform/framework/ask-ai'; import { SearchDialog } from '@inkform/framework/search-dialog'; +import { SecondaryTopNav } from '@inkform/framework/secondary-top-nav'; import type { DocsConfig } from '@inkform/framework'; -import { docTabs } from '@inkform/framework'; -import { loadDocsConfig } from '@inkform/framework/content'; import { apiBasePath } from '@/lib/route'; // ── Brand logo ──────────────────────────────────────────────────────────────── @@ -27,60 +26,6 @@ function Logo({ config }: { config: DocsConfig }) { ); } -// ── Tab nav ─────────────────────────────────────────────────────────────────── -// Rendered into the `topNav` slot, but pushed onto its own full-width row -// below the logo/search/assistant row by Galley's own theme.css (see the -// "header" section there) — verified on palm.mintlify.site: the tabs sit on -// a second line, left-aligned under the logo, underlined when active (no -// pill background). `.fw-topnav-tab`/`--active` classes are shared with -// every other theme; only the underline treatment is Galley-specific CSS. - -function TabNav({ config, activeTab }: { config: DocsConfig; activeTab: string }) { - const tabs = docTabs(config); - const apiBase = apiBasePath(config); - - return ( -
- {tabs.map((t) => { - const href = t.openapi && apiBase ? `/${apiBase}` : '/'; - const isActive = t.tab === activeTab; - return ( - - {t.tab} - - ); - })} -
- ); -} - -// ── Anchor links ────────────────────────────────────────────────────────────── - -function AnchorLinks({ config }: { config: DocsConfig }) { - const anchors = config.anchors ?? []; - if (anchors.length === 0) return null; - return ( -
- {anchors.map((a) => ( - - {a.name} - - ))} -
- ); -} - // ── Top bar props ───────────────────────────────────────────────────────────── export interface TopBarParts { @@ -93,43 +38,25 @@ export interface TopBarParts { /** * Build the three DocsShell header slot contents for Galley. - * Server component — client islands (SearchDialog, AskAi) are already - * marked 'use client' in the framework. - * - * Notes, verified directly on palm.mintlify.site: - * - No `` here — Palm's header carries no theme control at - * all; the theme switcher lives at the bottom of the sidebar instead - * (see components/sidebar-footer.tsx), wired into page.tsx via - * CollapsibleSidebar's `footer` slot. - * - The search trigger is wrapped in `.fw-galley-search-center` so it reads - * as centered in the header instead of flush right (CSS in theme.css). - * - `config.cta` is simply never set in this theme's docs.json — Palm's - * header has no CTA button at all. The render-if-present branch below is - * left in place (confirmed `buildTopBar` already no-ops with no `cta`) - * so a fork of this theme can still opt back in without touching this - * file. + * Server component — client islands (SearchDialog, AskAi, ScrollableTopNav) + * are already marked 'use client' in the framework. */ -export function buildTopBar(config: DocsConfig, activeTab: string): TopBarParts { +export function buildTopBar( + config: DocsConfig, + activeTab: string, + pathname = '/', +): TopBarParts { const aiEnabled = process.env.NEXT_PUBLIC_DOCS_AI_ENABLED === 'true'; const logo = ; const topNav = ( - <> - - - {(config.navbarLinks ?? []).map((l) => ( - - {l.name} - - ))} - + ); const topActions = ( diff --git a/examples/markdown-docs/app/[[...slug]]/page.tsx b/examples/markdown-docs/app/[[...slug]]/page.tsx index ff89b53..77d58bc 100644 --- a/examples/markdown-docs/app/[[...slug]]/page.tsx +++ b/examples/markdown-docs/app/[[...slug]]/page.tsx @@ -43,7 +43,8 @@ export default async function Page({ params }: { params: Promise<{ slug?: string const headings = extractHeadings(page.content); const { prev, next } = docNeighbours(config, ref.slug); const sidebar = sidebarForDoc(config, tab, ref.slug, renderIcon); - const topBar = buildTopBar(withContentNavLinks(config), tab.tab); + const pathname = (slug ?? []).length === 0 ? '/' : `/${(slug ?? []).join('/')}`; + const topBar = buildTopBar(withContentNavLinks(config), tab.tab, pathname); return ( diff --git a/examples/markdown-docs/app/blog/page.tsx b/examples/markdown-docs/app/blog/page.tsx index 7a04806..0d96026 100644 --- a/examples/markdown-docs/app/blog/page.tsx +++ b/examples/markdown-docs/app/blog/page.tsx @@ -9,7 +9,7 @@ export const metadata = { title: 'Blog' }; export default function BlogIndexPage() { const config = loadDocsConfig(); const posts = loadBlogPosts(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/blog') : null; return ( diff --git a/examples/markdown-docs/app/changelog/page.tsx b/examples/markdown-docs/app/changelog/page.tsx index 890f6a9..936434f 100644 --- a/examples/markdown-docs/app/changelog/page.tsx +++ b/examples/markdown-docs/app/changelog/page.tsx @@ -10,7 +10,7 @@ export const metadata = { title: 'Changelog' }; export default function ChangelogPage() { const config = loadDocsConfig(); const entries = loadChangelogEntries(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/changelog') : null; return ( diff --git a/examples/markdown-docs/app/theme.css b/examples/markdown-docs/app/theme.css index be59356..697b2b8 100644 --- a/examples/markdown-docs/app/theme.css +++ b/examples/markdown-docs/app/theme.css @@ -117,7 +117,7 @@ html.dark { row-gap: 0.625rem; align-items: center; padding-top: 0.875rem; - padding-bottom: 0.75rem; + padding-bottom: 0; } .fw-shell-header-start { @@ -192,7 +192,8 @@ html.dark { } .fw-topnav-tab { - padding: 0.4rem 0.0625rem 0.6rem; + padding: 0.5rem 0.0625rem 0.75rem; + margin-bottom: -1px; border: none; border-bottom: 2px solid transparent; border-radius: 0; diff --git a/examples/markdown-docs/components/top-bar.tsx b/examples/markdown-docs/components/top-bar.tsx index 953b83b..298b714 100644 --- a/examples/markdown-docs/components/top-bar.tsx +++ b/examples/markdown-docs/components/top-bar.tsx @@ -2,13 +2,10 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { AskAi } from '@inkform/framework/ask-ai'; import { SearchDialog } from '@inkform/framework/search-dialog'; +import { SecondaryTopNav } from '@inkform/framework/secondary-top-nav'; import type { DocsConfig } from '@inkform/framework'; -import { docTabs } from '@inkform/framework'; -import { loadDocsConfig } from '@inkform/framework/content'; import { apiBasePath } from '@/lib/route'; -// ── Brand logo ──────────────────────────────────────────────────────────────── - function Logo({ config }: { config: DocsConfig }) { const logo = typeof config.logo === 'string' @@ -27,109 +24,29 @@ function Logo({ config }: { config: DocsConfig }) { ); } -// ── Tab nav ─────────────────────────────────────────────────────────────────── -// Rendered into the `topNav` slot, but pushed onto its own full-width row -// below the logo/search/assistant row by Mono's own theme.css (see the -// "header" section there) — verified on palm.mintlify.site: the tabs sit on -// a second line, left-aligned under the logo, underlined when active (no -// pill background). `.fw-topnav-tab`/`--active` classes are shared with -// every other theme; only the underline treatment is Mono-specific CSS. - -function TabNav({ config, activeTab }: { config: DocsConfig; activeTab: string }) { - const tabs = docTabs(config); - const apiBase = apiBasePath(config); - - return ( -
- {tabs.map((t) => { - const href = t.openapi && apiBase ? `/${apiBase}` : '/'; - const isActive = t.tab === activeTab; - return ( - - {t.tab} - - ); - })} -
- ); -} - -// ── Anchor links ────────────────────────────────────────────────────────────── - -function AnchorLinks({ config }: { config: DocsConfig }) { - const anchors = config.anchors ?? []; - if (anchors.length === 0) return null; - return ( -
- {anchors.map((a) => ( - - {a.name} - - ))} -
- ); -} - -// ── Top bar props ───────────────────────────────────────────────────────────── - export interface TopBarParts { logo: ReactNode; topNav: ReactNode; topActions: ReactNode; - /** Separate from topActions so DocsShell can duplicate just this into the mobile drawer without also duplicating the (stateful) search/AI widgets. */ cta: ReactNode; } -/** - * Build the three DocsShell header slot contents for Mono. - * Server component — client islands (SearchDialog, AskAi) are already - * marked 'use client' in the framework. - * - * Notes, verified directly on palm.mintlify.site: - * - No `` here — Palm's header carries no theme control at - * all; the theme switcher lives at the bottom of the sidebar instead - * (see components/sidebar-footer.tsx), wired into page.tsx via - * CollapsibleSidebar's `footer` slot. - * - The search trigger is wrapped in `.fw-mono-search-center` so it reads - * as centered in the header instead of flush right (CSS in theme.css). - * - `config.cta` is simply never set in this theme's docs.json — Palm's - * header has no CTA button at all. The render-if-present branch below is - * left in place (confirmed `buildTopBar` already no-ops with no `cta`) - * so a fork of this theme can still opt back in without touching this - * file. - */ -export function buildTopBar(config: DocsConfig, activeTab: string): TopBarParts { +export function buildTopBar( + config: DocsConfig, + activeTab: string, + pathname = '/', +): TopBarParts { const aiEnabled = process.env.NEXT_PUBLIC_DOCS_AI_ENABLED === 'true'; const logo = ; const topNav = ( - <> - - - {(config.navbarLinks ?? []).map((l) => ( - - {l.name} - - ))} - + ); const topActions = ( diff --git a/examples/pokeapi-docs/app/[[...slug]]/page.tsx b/examples/pokeapi-docs/app/[[...slug]]/page.tsx index ff89b53..77d58bc 100644 --- a/examples/pokeapi-docs/app/[[...slug]]/page.tsx +++ b/examples/pokeapi-docs/app/[[...slug]]/page.tsx @@ -43,7 +43,8 @@ export default async function Page({ params }: { params: Promise<{ slug?: string const headings = extractHeadings(page.content); const { prev, next } = docNeighbours(config, ref.slug); const sidebar = sidebarForDoc(config, tab, ref.slug, renderIcon); - const topBar = buildTopBar(withContentNavLinks(config), tab.tab); + const pathname = (slug ?? []).length === 0 ? '/' : `/${(slug ?? []).join('/')}`; + const topBar = buildTopBar(withContentNavLinks(config), tab.tab, pathname); return ( diff --git a/examples/pokeapi-docs/app/blog/page.tsx b/examples/pokeapi-docs/app/blog/page.tsx index 7a04806..0d96026 100644 --- a/examples/pokeapi-docs/app/blog/page.tsx +++ b/examples/pokeapi-docs/app/blog/page.tsx @@ -9,7 +9,7 @@ export const metadata = { title: 'Blog' }; export default function BlogIndexPage() { const config = loadDocsConfig(); const posts = loadBlogPosts(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/blog') : null; return ( diff --git a/examples/pokeapi-docs/app/changelog/page.tsx b/examples/pokeapi-docs/app/changelog/page.tsx index 890f6a9..936434f 100644 --- a/examples/pokeapi-docs/app/changelog/page.tsx +++ b/examples/pokeapi-docs/app/changelog/page.tsx @@ -10,7 +10,7 @@ export const metadata = { title: 'Changelog' }; export default function ChangelogPage() { const config = loadDocsConfig(); const entries = loadChangelogEntries(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/changelog') : null; return ( diff --git a/examples/pokeapi-docs/app/theme.css b/examples/pokeapi-docs/app/theme.css index 00e473f..af24cce 100644 --- a/examples/pokeapi-docs/app/theme.css +++ b/examples/pokeapi-docs/app/theme.css @@ -96,7 +96,7 @@ html.dark { row-gap: 0.625rem; align-items: center; padding-top: 0.875rem; - padding-bottom: 0.75rem; + padding-bottom: 0; } .fw-shell-header-start { @@ -171,7 +171,8 @@ html.dark { } .fw-topnav-tab { - padding: 0.4rem 0.0625rem 0.6rem; + padding: 0.5rem 0.0625rem 0.75rem; + margin-bottom: -1px; border: none; border-bottom: 2px solid transparent; border-radius: 0; diff --git a/examples/pokeapi-docs/components/top-bar.tsx b/examples/pokeapi-docs/components/top-bar.tsx index b8df02c..ec87b9e 100644 --- a/examples/pokeapi-docs/components/top-bar.tsx +++ b/examples/pokeapi-docs/components/top-bar.tsx @@ -2,13 +2,10 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { AskAi } from '@inkform/framework/ask-ai'; import { SearchDialog } from '@inkform/framework/search-dialog'; +import { SecondaryTopNav } from '@inkform/framework/secondary-top-nav'; import type { DocsConfig } from '@inkform/framework'; -import { docTabs } from '@inkform/framework'; -import { loadDocsConfig } from '@inkform/framework/content'; import { apiBasePath } from '@/lib/route'; -// ── Brand logo ──────────────────────────────────────────────────────────────── - function Logo({ config }: { config: DocsConfig }) { const logo = typeof config.logo === 'string' @@ -27,109 +24,29 @@ function Logo({ config }: { config: DocsConfig }) { ); } -// ── Tab nav ─────────────────────────────────────────────────────────────────── -// Rendered into the `topNav` slot, but pushed onto its own full-width row -// below the logo/search/assistant row by Aurora's own theme.css (see the -// "header" section there) — verified on palm.mintlify.site: the tabs sit on -// a second line, left-aligned under the logo, underlined when active (no -// pill background). `.fw-topnav-tab`/`--active` classes are shared with -// every other theme; only the underline treatment is Aurora-specific CSS. - -function TabNav({ config, activeTab }: { config: DocsConfig; activeTab: string }) { - const tabs = docTabs(config); - const apiBase = apiBasePath(config); - - return ( -
- {tabs.map((t) => { - const href = t.openapi && apiBase ? `/${apiBase}` : '/'; - const isActive = t.tab === activeTab; - return ( - - {t.tab} - - ); - })} -
- ); -} - -// ── Anchor links ────────────────────────────────────────────────────────────── - -function AnchorLinks({ config }: { config: DocsConfig }) { - const anchors = config.anchors ?? []; - if (anchors.length === 0) return null; - return ( -
- {anchors.map((a) => ( - - {a.name} - - ))} -
- ); -} - -// ── Top bar props ───────────────────────────────────────────────────────────── - export interface TopBarParts { logo: ReactNode; topNav: ReactNode; topActions: ReactNode; - /** Separate from topActions so DocsShell can duplicate just this into the mobile drawer without also duplicating the (stateful) search/AI widgets. */ cta: ReactNode; } -/** - * Build the three DocsShell header slot contents for Aurora. - * Server component — client islands (SearchDialog, AskAi) are already - * marked 'use client' in the framework. - * - * Notes, verified directly on palm.mintlify.site: - * - No `` here — Palm's header carries no theme control at - * all; the theme switcher lives at the bottom of the sidebar instead - * (see components/sidebar-footer.tsx), wired into page.tsx via - * CollapsibleSidebar's `footer` slot. - * - The search trigger is wrapped in `.fw-aurora-search-center` so it reads - * as centered in the header instead of flush right (CSS in theme.css). - * - `config.cta` is simply never set in this theme's docs.json — Palm's - * header has no CTA button at all. The render-if-present branch below is - * left in place (confirmed `buildTopBar` already no-ops with no `cta`) - * so a fork of this theme can still opt back in without touching this - * file. - */ -export function buildTopBar(config: DocsConfig, activeTab: string): TopBarParts { +export function buildTopBar( + config: DocsConfig, + activeTab: string, + pathname = '/', +): TopBarParts { const aiEnabled = process.env.NEXT_PUBLIC_DOCS_AI_ENABLED === 'true'; const logo = ; const topNav = ( - <> - - - {(config.navbarLinks ?? []).map((l) => ( - - {l.name} - - ))} - + ); const topActions = ( diff --git a/packages/framework/package.json b/packages/framework/package.json index 722c378..4d27b2c 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -55,7 +55,8 @@ "./theme-toggle": "./src/theme-toggle.tsx", "./subscribe-form": "./src/subscribe-form.tsx", "./analytics-script": "./src/analytics-script.tsx", - "./reactions": "./src/reactions.tsx", + "./secondary-top-nav": "./src/secondary-top-nav.tsx", + "./scrollable-top-nav": "./src/scrollable-top-nav.tsx", "./comments": "./src/comments.tsx" }, "files": [ diff --git a/packages/framework/src/scrollable-top-nav.tsx b/packages/framework/src/scrollable-top-nav.tsx new file mode 100644 index 0000000..1fe0886 --- /dev/null +++ b/packages/framework/src/scrollable-top-nav.tsx @@ -0,0 +1,74 @@ +'use client'; + +import * as React from 'react'; + +function ChevronLeft() { + return ( + + + + ); +} + +function ChevronRight() { + return ( + + + + ); +} + +/** + * Horizontally scrollable top-nav row with edge fade + chevron hints on mobile. + * Hints appear only when there is more content in that direction. + */ +export function ScrollableTopNav({ children }: { children: React.ReactNode }) { + const trackRef = React.useRef(null); + const [canScrollLeft, setCanScrollLeft] = React.useState(false); + const [canScrollRight, setCanScrollRight] = React.useState(false); + + const update = React.useCallback(() => { + const el = trackRef.current; + if (!el) return; + const { scrollLeft, scrollWidth, clientWidth } = el; + setCanScrollLeft(scrollLeft > 1); + setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 1); + }, []); + + React.useEffect(() => { + update(); + const el = trackRef.current; + if (!el) return; + + el.addEventListener('scroll', update, { passive: true }); + const ro = new ResizeObserver(update); + ro.observe(el); + + return () => { + el.removeEventListener('scroll', update); + ro.disconnect(); + }; + }, [update, children]); + + const className = [ + 'fw-topnav-scroll', + canScrollLeft ? 'fw-topnav-scroll--can-left' : '', + canScrollRight ? 'fw-topnav-scroll--can-right' : '', + ] + .filter(Boolean) + .join(' '); + + return ( +
+
+ {children} +
+
+ +
+
+ +
+
+ ); +} diff --git a/packages/framework/src/secondary-top-nav.tsx b/packages/framework/src/secondary-top-nav.tsx new file mode 100644 index 0000000..0924fc2 --- /dev/null +++ b/packages/framework/src/secondary-top-nav.tsx @@ -0,0 +1,95 @@ +import type { DocsConfig } from './nav'; +import { docTabs } from './nav'; +import { ScrollableTopNav } from './scrollable-top-nav'; + +export type SecondaryTopNavProps = { + config: DocsConfig; + /** Active docs tab label (from route resolution). Empty on blog/changelog pages. */ + activeTab: string; + /** Current pathname, e.g. `/blog`, `/changelog`, `/guides/docs`. */ + pathname: string; + /** Slug base for the OpenAPI tab link, e.g. `api-reference`. Omit when none. */ + apiBase?: string | null; +}; + +function isInternalNavActive(pathname: string, href: string): boolean { + if (href.startsWith('http')) return false; + if (href === '/') return pathname === '/' || pathname === ''; + return pathname === href || pathname.startsWith(`${href}/`); +} + +function tabClass(active: boolean) { + return `fw-topnav-tab${active ? ' fw-topnav-tab--active' : ''}`; +} + +/** + * Unified secondary header row: doc tabs, anchors, and navbar links share the + * same underline-tab styling and active state logic. + */ +export function SecondaryTopNav({ config, activeTab, pathname, apiBase }: SecondaryTopNavProps) { + const tabs = docTabs(config); + const anchors = config.anchors ?? []; + const navlinks = config.navbarLinks ?? []; + + const onBlog = pathname.startsWith('/blog'); + const onChangelog = pathname.startsWith('/changelog'); + const onDocs = !onBlog && !onChangelog; + + return ( + +
+ {tabs.map((t) => { + const href = t.openapi && apiBase ? `/${apiBase}` : '/'; + const onApiRoute = + !!apiBase && (pathname === `/${apiBase}` || pathname.startsWith(`/${apiBase}/`)); + const isActive = t.openapi ? onApiRoute : onDocs && t.tab === activeTab; + + return ( + + {t.tab} + + ); + })} + + {anchors.map((a) => { + const external = a.href.startsWith('http'); + const isActive = isInternalNavActive(pathname, a.href); + + return ( + + {a.name} + + ); + })} + + {navlinks.map((l) => { + const external = l.href.startsWith('http'); + const isActive = isInternalNavActive(pathname, l.href); + + return ( + + {l.name} + + ); + })} +
+
+ ); +} diff --git a/packages/framework/src/styles/layout.css b/packages/framework/src/styles/layout.css index 1720ada..4b8eb64 100644 --- a/packages/framework/src/styles/layout.css +++ b/packages/framework/src/styles/layout.css @@ -77,18 +77,85 @@ display: flex; align-items: center; gap: var(--fw-space-1); - /* The tab row used to just `overflow: hidden` on its ancestor, silently - clipping tabs with no way to reach them — confirmed by measurement at - tablet width (~834px). Scrolls instead now, with the scrollbar hidden - since this is a nav affordance, not a content area. */ + flex-shrink: 0; + white-space: nowrap; + /* Legacy: themes without ScrollableTopNav still scroll at narrow widths */ overflow-x: auto; scrollbar-width: none; } +.fw-topnav-scroll .fw-topnav-tabs { + overflow-x: visible; +} + .fw-topnav-tabs::-webkit-scrollbar { display: none; } +/* Scrollable secondary nav — mobile edge hints live on the wrapper */ +.fw-topnav-scroll { + position: relative; + min-width: 0; + width: 100%; +} + +.fw-topnav-scroll-track { + min-width: 0; +} + +.fw-topnav-scroll-hint { + display: none; +} + +@media (max-width: 60rem) { + .fw-topnav-scroll-track { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + + .fw-topnav-scroll-track::-webkit-scrollbar { + display: none; + } + + .fw-topnav-scroll-hint { + position: absolute; + top: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + width: 2.25rem; + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + } + + .fw-topnav-scroll-hint svg { + width: 14px; + height: 14px; + color: var(--fw-muted); + } + + .fw-topnav-scroll-hint--left { + left: 0; + padding-right: 0.5rem; + background: linear-gradient(to right, var(--fw-bg) 45%, transparent); + } + + .fw-topnav-scroll-hint--right { + right: 0; + padding-left: 0.5rem; + background: linear-gradient(to left, var(--fw-bg) 45%, transparent); + } + + .fw-topnav-scroll--can-left .fw-topnav-scroll-hint--left, + .fw-topnav-scroll--can-right .fw-topnav-scroll-hint--right { + opacity: 1; + } +} + .fw-topnav-tab { padding: 0.375rem 0.75rem; font-size: 0.875rem; @@ -185,18 +252,10 @@ letter-spacing: -0.01em; } -/* Hide secondary top-nav links from the HEADER specifically on mobile - (prevents overflow in the now-cramped row) — the tab row itself scrolls - (above) rather than needing to hide. Scoped to .fw-shell-header, not - global: DocsShell also renders topNav (and cta, if given) a second time - inside the hamburger drawer — see .fw-drawer-nav below and - MobileSidebarToggle in shell-client.tsx — so hiding them here doesn't - remove them from reach entirely. It used to: plain display:none with no - second copy anywhere meant these links were completely inaccessible on - mobile, not just relocated. */ +/* Hide header CTA on mobile (relocated to drawer). Secondary nav links stay + visible in the scrollable second row — anchors/navlinks now share + .fw-topnav-tab styling inside SecondaryTopNav. */ @media (max-width: 60rem) { - .fw-shell-header .fw-topnav-anchors, - .fw-shell-header .fw-topnav-navlink, .fw-shell-header .fw-topnav-cta { display: none; } diff --git a/templates/canopy/app/[[...slug]]/page.tsx b/templates/canopy/app/[[...slug]]/page.tsx index ff89b53..77d58bc 100644 --- a/templates/canopy/app/[[...slug]]/page.tsx +++ b/templates/canopy/app/[[...slug]]/page.tsx @@ -43,7 +43,8 @@ export default async function Page({ params }: { params: Promise<{ slug?: string const headings = extractHeadings(page.content); const { prev, next } = docNeighbours(config, ref.slug); const sidebar = sidebarForDoc(config, tab, ref.slug, renderIcon); - const topBar = buildTopBar(withContentNavLinks(config), tab.tab); + const pathname = (slug ?? []).length === 0 ? '/' : `/${(slug ?? []).join('/')}`; + const topBar = buildTopBar(withContentNavLinks(config), tab.tab, pathname); return ( diff --git a/templates/canopy/app/blog/page.tsx b/templates/canopy/app/blog/page.tsx index 7a04806..0d96026 100644 --- a/templates/canopy/app/blog/page.tsx +++ b/templates/canopy/app/blog/page.tsx @@ -9,7 +9,7 @@ export const metadata = { title: 'Blog' }; export default function BlogIndexPage() { const config = loadDocsConfig(); const posts = loadBlogPosts(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/blog') : null; return ( diff --git a/templates/canopy/app/changelog/page.tsx b/templates/canopy/app/changelog/page.tsx index 890f6a9..936434f 100644 --- a/templates/canopy/app/changelog/page.tsx +++ b/templates/canopy/app/changelog/page.tsx @@ -10,7 +10,7 @@ export const metadata = { title: 'Changelog' }; export default function ChangelogPage() { const config = loadDocsConfig(); const entries = loadChangelogEntries(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/changelog') : null; return ( diff --git a/templates/canopy/app/theme.css b/templates/canopy/app/theme.css index 5da81e3..19970b1 100644 --- a/templates/canopy/app/theme.css +++ b/templates/canopy/app/theme.css @@ -94,7 +94,7 @@ html.dark { row-gap: 0.625rem; align-items: center; padding-top: 0.875rem; - padding-bottom: 0.75rem; + padding-bottom: 0; } .fw-shell-header-start { @@ -169,7 +169,8 @@ html.dark { } .fw-topnav-tab { - padding: 0.4rem 0.0625rem 0.6rem; + padding: 0.5rem 0.0625rem 0.75rem; + margin-bottom: -1px; border: none; border-bottom: 2px solid transparent; border-radius: 0; diff --git a/templates/canopy/components/top-bar.tsx b/templates/canopy/components/top-bar.tsx index f72b924..1abfc59 100644 --- a/templates/canopy/components/top-bar.tsx +++ b/templates/canopy/components/top-bar.tsx @@ -2,13 +2,10 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { AskAi } from '@inkform/framework/ask-ai'; import { SearchDialog } from '@inkform/framework/search-dialog'; +import { SecondaryTopNav } from '@inkform/framework/secondary-top-nav'; import type { DocsConfig } from '@inkform/framework'; -import { docTabs } from '@inkform/framework'; -import { loadDocsConfig } from '@inkform/framework/content'; import { apiBasePath } from '@/lib/route'; -// ── Brand logo ──────────────────────────────────────────────────────────────── - function Logo({ config }: { config: DocsConfig }) { const logo = typeof config.logo === 'string' @@ -27,109 +24,29 @@ function Logo({ config }: { config: DocsConfig }) { ); } -// ── Tab nav ─────────────────────────────────────────────────────────────────── -// Rendered into the `topNav` slot, but pushed onto its own full-width row -// below the logo/search/assistant row by Canopy's own theme.css (see the -// "header" section there) — verified on palm.mintlify.site: the tabs sit on -// a second line, left-aligned under the logo, underlined when active (no -// pill background). `.fw-topnav-tab`/`--active` classes are shared with -// every other theme; only the underline treatment is Canopy-specific CSS. - -function TabNav({ config, activeTab }: { config: DocsConfig; activeTab: string }) { - const tabs = docTabs(config); - const apiBase = apiBasePath(config); - - return ( -
- {tabs.map((t) => { - const href = t.openapi && apiBase ? `/${apiBase}` : '/'; - const isActive = t.tab === activeTab; - return ( - - {t.tab} - - ); - })} -
- ); -} - -// ── Anchor links ────────────────────────────────────────────────────────────── - -function AnchorLinks({ config }: { config: DocsConfig }) { - const anchors = config.anchors ?? []; - if (anchors.length === 0) return null; - return ( -
- {anchors.map((a) => ( - - {a.name} - - ))} -
- ); -} - -// ── Top bar props ───────────────────────────────────────────────────────────── - export interface TopBarParts { logo: ReactNode; topNav: ReactNode; topActions: ReactNode; - /** Separate from topActions so DocsShell can duplicate just this into the mobile drawer without also duplicating the (stateful) search/AI widgets. */ cta: ReactNode; } -/** - * Build the three DocsShell header slot contents for Canopy. - * Server component — client islands (SearchDialog, AskAi) are already - * marked 'use client' in the framework. - * - * Notes, verified directly on palm.mintlify.site: - * - No `` here — Palm's header carries no theme control at - * all; the theme switcher lives at the bottom of the sidebar instead - * (see components/sidebar-footer.tsx), wired into page.tsx via - * CollapsibleSidebar's `footer` slot. - * - The search trigger is wrapped in `.fw-canopy-search-center` so it reads - * as centered in the header instead of flush right (CSS in theme.css). - * - `config.cta` is simply never set in this theme's docs.json — Palm's - * header has no CTA button at all. The render-if-present branch below is - * left in place (confirmed `buildTopBar` already no-ops with no `cta`) - * so a fork of this theme can still opt back in without touching this - * file. - */ -export function buildTopBar(config: DocsConfig, activeTab: string): TopBarParts { +export function buildTopBar( + config: DocsConfig, + activeTab: string, + pathname = '/', +): TopBarParts { const aiEnabled = process.env.NEXT_PUBLIC_DOCS_AI_ENABLED === 'true'; const logo = ; const topNav = ( - <> - - - {(config.navbarLinks ?? []).map((l) => ( - - {l.name} - - ))} - + ); const topActions = ( diff --git a/templates/galley/app/[[...slug]]/page.tsx b/templates/galley/app/[[...slug]]/page.tsx index ff89b53..77d58bc 100644 --- a/templates/galley/app/[[...slug]]/page.tsx +++ b/templates/galley/app/[[...slug]]/page.tsx @@ -43,7 +43,8 @@ export default async function Page({ params }: { params: Promise<{ slug?: string const headings = extractHeadings(page.content); const { prev, next } = docNeighbours(config, ref.slug); const sidebar = sidebarForDoc(config, tab, ref.slug, renderIcon); - const topBar = buildTopBar(withContentNavLinks(config), tab.tab); + const pathname = (slug ?? []).length === 0 ? '/' : `/${(slug ?? []).join('/')}`; + const topBar = buildTopBar(withContentNavLinks(config), tab.tab, pathname); return ( diff --git a/templates/galley/app/blog/page.tsx b/templates/galley/app/blog/page.tsx index 7a04806..0d96026 100644 --- a/templates/galley/app/blog/page.tsx +++ b/templates/galley/app/blog/page.tsx @@ -9,7 +9,7 @@ export const metadata = { title: 'Blog' }; export default function BlogIndexPage() { const config = loadDocsConfig(); const posts = loadBlogPosts(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/blog') : null; return ( diff --git a/templates/galley/app/changelog/page.tsx b/templates/galley/app/changelog/page.tsx index 890f6a9..936434f 100644 --- a/templates/galley/app/changelog/page.tsx +++ b/templates/galley/app/changelog/page.tsx @@ -10,7 +10,7 @@ export const metadata = { title: 'Changelog' }; export default function ChangelogPage() { const config = loadDocsConfig(); const entries = loadChangelogEntries(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/changelog') : null; return ( diff --git a/templates/galley/app/theme.css b/templates/galley/app/theme.css index f4c28f2..163f887 100644 --- a/templates/galley/app/theme.css +++ b/templates/galley/app/theme.css @@ -124,7 +124,7 @@ html.dark { row-gap: 0.625rem; align-items: center; padding-top: 0.875rem; - padding-bottom: 0.75rem; + padding-bottom: 0; } .fw-shell-header-start { @@ -199,7 +199,8 @@ html.dark { } .fw-topnav-tab { - padding: 0.4rem 0.0625rem 0.6rem; + padding: 0.5rem 0.0625rem 0.75rem; + margin-bottom: -1px; border: none; border-bottom: 2px solid transparent; border-radius: 0; diff --git a/templates/galley/components/top-bar.tsx b/templates/galley/components/top-bar.tsx index 13d637f..9fc9f5c 100644 --- a/templates/galley/components/top-bar.tsx +++ b/templates/galley/components/top-bar.tsx @@ -2,13 +2,10 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { AskAi } from '@inkform/framework/ask-ai'; import { SearchDialog } from '@inkform/framework/search-dialog'; +import { SecondaryTopNav } from '@inkform/framework/secondary-top-nav'; import type { DocsConfig } from '@inkform/framework'; -import { docTabs } from '@inkform/framework'; -import { loadDocsConfig } from '@inkform/framework/content'; import { apiBasePath } from '@/lib/route'; -// ── Brand logo ──────────────────────────────────────────────────────────────── - function Logo({ config }: { config: DocsConfig }) { const logo = typeof config.logo === 'string' @@ -27,109 +24,29 @@ function Logo({ config }: { config: DocsConfig }) { ); } -// ── Tab nav ─────────────────────────────────────────────────────────────────── -// Rendered into the `topNav` slot, but pushed onto its own full-width row -// below the logo/search/assistant row by Galley's own theme.css (see the -// "header" section there) — verified on palm.mintlify.site: the tabs sit on -// a second line, left-aligned under the logo, underlined when active (no -// pill background). `.fw-topnav-tab`/`--active` classes are shared with -// every other theme; only the underline treatment is Galley-specific CSS. - -function TabNav({ config, activeTab }: { config: DocsConfig; activeTab: string }) { - const tabs = docTabs(config); - const apiBase = apiBasePath(config); - - return ( -
- {tabs.map((t) => { - const href = t.openapi && apiBase ? `/${apiBase}` : '/'; - const isActive = t.tab === activeTab; - return ( - - {t.tab} - - ); - })} -
- ); -} - -// ── Anchor links ────────────────────────────────────────────────────────────── - -function AnchorLinks({ config }: { config: DocsConfig }) { - const anchors = config.anchors ?? []; - if (anchors.length === 0) return null; - return ( -
- {anchors.map((a) => ( - - {a.name} - - ))} -
- ); -} - -// ── Top bar props ───────────────────────────────────────────────────────────── - export interface TopBarParts { logo: ReactNode; topNav: ReactNode; topActions: ReactNode; - /** Separate from topActions so DocsShell can duplicate just this into the mobile drawer without also duplicating the (stateful) search/AI widgets. */ cta: ReactNode; } -/** - * Build the three DocsShell header slot contents for Galley. - * Server component — client islands (SearchDialog, AskAi) are already - * marked 'use client' in the framework. - * - * Notes, verified directly on palm.mintlify.site: - * - No `` here — Palm's header carries no theme control at - * all; the theme switcher lives at the bottom of the sidebar instead - * (see components/sidebar-footer.tsx), wired into page.tsx via - * CollapsibleSidebar's `footer` slot. - * - The search trigger is wrapped in `.fw-galley-search-center` so it reads - * as centered in the header instead of flush right (CSS in theme.css). - * - `config.cta` is simply never set in this theme's docs.json — Palm's - * header has no CTA button at all. The render-if-present branch below is - * left in place (confirmed `buildTopBar` already no-ops with no `cta`) - * so a fork of this theme can still opt back in without touching this - * file. - */ -export function buildTopBar(config: DocsConfig, activeTab: string): TopBarParts { +export function buildTopBar( + config: DocsConfig, + activeTab: string, + pathname = '/', +): TopBarParts { const aiEnabled = process.env.NEXT_PUBLIC_DOCS_AI_ENABLED === 'true'; const logo = ; const topNav = ( - <> - - - {(config.navbarLinks ?? []).map((l) => ( - - {l.name} - - ))} - + ); const topActions = ( diff --git a/templates/shadcn/app/[[...slug]]/page.tsx b/templates/shadcn/app/[[...slug]]/page.tsx index ff89b53..77d58bc 100644 --- a/templates/shadcn/app/[[...slug]]/page.tsx +++ b/templates/shadcn/app/[[...slug]]/page.tsx @@ -43,7 +43,8 @@ export default async function Page({ params }: { params: Promise<{ slug?: string const headings = extractHeadings(page.content); const { prev, next } = docNeighbours(config, ref.slug); const sidebar = sidebarForDoc(config, tab, ref.slug, renderIcon); - const topBar = buildTopBar(withContentNavLinks(config), tab.tab); + const pathname = (slug ?? []).length === 0 ? '/' : `/${(slug ?? []).join('/')}`; + const topBar = buildTopBar(withContentNavLinks(config), tab.tab, pathname); return ( diff --git a/templates/shadcn/app/blog/page.tsx b/templates/shadcn/app/blog/page.tsx index 7a04806..0d96026 100644 --- a/templates/shadcn/app/blog/page.tsx +++ b/templates/shadcn/app/blog/page.tsx @@ -9,7 +9,7 @@ export const metadata = { title: 'Blog' }; export default function BlogIndexPage() { const config = loadDocsConfig(); const posts = loadBlogPosts(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/blog') : null; return ( diff --git a/templates/shadcn/app/changelog/page.tsx b/templates/shadcn/app/changelog/page.tsx index 890f6a9..936434f 100644 --- a/templates/shadcn/app/changelog/page.tsx +++ b/templates/shadcn/app/changelog/page.tsx @@ -10,7 +10,7 @@ export const metadata = { title: 'Changelog' }; export default function ChangelogPage() { const config = loadDocsConfig(); const entries = loadChangelogEntries(); - const topBar = config ? buildTopBar(withContentNavLinks(config), '') : null; + const topBar = config ? buildTopBar(withContentNavLinks(config), '', '/changelog') : null; return ( diff --git a/templates/shadcn/app/theme.css b/templates/shadcn/app/theme.css index 9658876..c2fec5f 100644 --- a/templates/shadcn/app/theme.css +++ b/templates/shadcn/app/theme.css @@ -119,7 +119,7 @@ html.dark { row-gap: 0.625rem; align-items: center; padding-top: 0.875rem; - padding-bottom: 0.75rem; + padding-bottom: 0; } .fw-shell-header-start { @@ -194,7 +194,8 @@ html.dark { } .fw-topnav-tab { - padding: 0.4rem 0.0625rem 0.6rem; + padding: 0.5rem 0.0625rem 0.75rem; + margin-bottom: -1px; border: none; border-bottom: 2px solid transparent; border-radius: 0; diff --git a/templates/shadcn/components/top-bar.tsx b/templates/shadcn/components/top-bar.tsx index 4ca3a6d..f563f4f 100644 --- a/templates/shadcn/components/top-bar.tsx +++ b/templates/shadcn/components/top-bar.tsx @@ -2,13 +2,10 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { AskAi } from '@inkform/framework/ask-ai'; import { SearchDialog } from '@inkform/framework/search-dialog'; +import { SecondaryTopNav } from '@inkform/framework/secondary-top-nav'; import type { DocsConfig } from '@inkform/framework'; -import { docTabs } from '@inkform/framework'; -import { loadDocsConfig } from '@inkform/framework/content'; import { apiBasePath } from '@/lib/route'; -// ── Brand logo ──────────────────────────────────────────────────────────────── - function Logo({ config }: { config: DocsConfig }) { const logo = typeof config.logo === 'string' @@ -27,109 +24,29 @@ function Logo({ config }: { config: DocsConfig }) { ); } -// ── Tab nav ─────────────────────────────────────────────────────────────────── -// Rendered into the `topNav` slot, but pushed onto its own full-width row -// below the logo/search/assistant row by Shadcn's own theme.css (see the -// "header" section there) — verified on palm.mintlify.site: the tabs sit on -// a second line, left-aligned under the logo, underlined when active (no -// pill background). `.fw-topnav-tab`/`--active` classes are shared with -// every other theme; only the underline treatment is Shadcn-specific CSS. - -function TabNav({ config, activeTab }: { config: DocsConfig; activeTab: string }) { - const tabs = docTabs(config); - const apiBase = apiBasePath(config); - - return ( -
- {tabs.map((t) => { - const href = t.openapi && apiBase ? `/${apiBase}` : '/'; - const isActive = t.tab === activeTab; - return ( - - {t.tab} - - ); - })} -
- ); -} - -// ── Anchor links ────────────────────────────────────────────────────────────── - -function AnchorLinks({ config }: { config: DocsConfig }) { - const anchors = config.anchors ?? []; - if (anchors.length === 0) return null; - return ( -
- {anchors.map((a) => ( - - {a.name} - - ))} -
- ); -} - -// ── Top bar props ───────────────────────────────────────────────────────────── - export interface TopBarParts { logo: ReactNode; topNav: ReactNode; topActions: ReactNode; - /** Separate from topActions so DocsShell can duplicate just this into the mobile drawer without also duplicating the (stateful) search/AI widgets. */ cta: ReactNode; } -/** - * Build the three DocsShell header slot contents for Shadcn. - * Server component — client islands (SearchDialog, AskAi) are already - * marked 'use client' in the framework. - * - * Notes, verified directly on palm.mintlify.site: - * - No `` here — Palm's header carries no theme control at - * all; the theme switcher lives at the bottom of the sidebar instead - * (see components/sidebar-footer.tsx), wired into page.tsx via - * CollapsibleSidebar's `footer` slot. - * - The search trigger is wrapped in `.fw-shadcn-search-center` so it reads - * as centered in the header instead of flush right (CSS in theme.css). - * - `config.cta` is simply never set in this theme's docs.json — Palm's - * header has no CTA button at all. The render-if-present branch below is - * left in place (confirmed `buildTopBar` already no-ops with no `cta`) - * so a fork of this theme can still opt back in without touching this - * file. - */ -export function buildTopBar(config: DocsConfig, activeTab: string): TopBarParts { +export function buildTopBar( + config: DocsConfig, + activeTab: string, + pathname = '/', +): TopBarParts { const aiEnabled = process.env.NEXT_PUBLIC_DOCS_AI_ENABLED === 'true'; const logo = ; const topNav = ( - <> - - - {(config.navbarLinks ?? []).map((l) => ( - - {l.name} - - ))} - + ); const topActions = (