From 591479a1f3c1f7ee362141a7735f996caf536447 Mon Sep 17 00:00:00 2001 From: Chase Gray Date: Tue, 10 Nov 2026 12:55:07 -0800 Subject: [PATCH 01/20] feat(ia): add operate, sdks & cli and guides sections Group the docs into six nav areas over the existing sections. Node, upgrade and changelog pages move from Tempo EVM to a new Operate section; SDK, CLI and server pages move from Get Started to SDKs & CLI; the payment guides move to Guides. URLs are unchanged. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 9 +- public/SKILL.md | 11 +- src/lib/ai-docs.test.ts | 11 +- src/lib/ai-docs.ts | 10 +- src/lib/docs-sections.ts | 94 +++++- src/lib/og-sections.test.ts | 19 +- src/lib/og-sections.ts | 6 + src/lib/sidebar-coverage.test.ts | 62 ++-- vocs.config.ts | 492 ++++++++++++++----------------- 9 files changed, 404 insertions(+), 310 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02cf4b753..d1b9b2e80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,11 @@ Vocs-powered documentation site for Tempo protocol. Use this guidance when creat ## Documentation IA and agent exports -- `src/lib/docs-sections.ts` owns the section labels, landing routes, and route ownership; `vocs.config.ts` owns the sidebar within each section. -- Primary sections: Get Started (`/get-started`), Accounts (`/docs/accounts`), Earn (`/docs/earn`), Routes (`/docs/routes`), Zones (`/docs/zones`), and Machine Payments (`/docs/agents`). Get Started also owns the SDK, CLI, wallet CLI, and server handler pages (`/docs/sdk`, `/docs/cli`, `/docs/wallet`, `/docs/server`) as its Tempo CLI and Tempo SDK sections. -- Tempo EVM (`/docs/development`) covers wallets and signing keys, transactions, fees, TIP-20 Tokens, contracts, exchange, network details, nodes, protocol specifications, and changelog. -- API reference (`/docs/api`) is its own top-nav link, matching Console's "API Docs" link. Partners (`/docs/partners`) contains third-party integrations. The old APIs & SDKs hub (`/docs/tools`) redirects to `/docs/sdk`. +- `src/lib/docs-sections.ts` owns the section labels, landing routes, and route ownership, plus `docsAreas`, the six global-nav areas that group the sections; `vocs.config.ts` owns the sidebar within each section. +- Global nav areas: Get started, Products (Accounts, Earn, Routes, Zones, Machine Payments), Tempo EVM, Operate, Reference (API reference, SDKs & CLI) and Resources (Guides, Partners). The second header row lists the sections of the current area. +- Sections: Get Started (`/get-started`, onboarding only), Accounts (`/docs/accounts`), Earn (`/docs/earn`), Routes (`/docs/routes`), Zones (`/docs/zones`), Machine Payments (`/docs/agents`), Tempo EVM (`/docs/development`), Operate (`/docs/guide/node`, `/docs/changelog`, `/docs/protocol/upgrades`), API reference (`/docs/api`), SDKs & CLI (`/docs/sdk`, `/docs/cli`, `/docs/wallet`, `/docs/server`), Guides (`/docs/resources`) and Partners (`/docs/partners`). +- Tempo EVM covers wallets and signing keys, transactions, fees, TIP-20 Tokens, contracts, exchange, network details, and protocol specifications. Operate covers running nodes and validators, network upgrades, and node releases. +- API reference matches Console's "API Docs" link. Partners contains third-party integrations. The old APIs & SDKs hub (`/docs/tools`) redirects to `/docs/sdk`. - Keep Earn and Routes beta notices and the Zones availability notice (live on Mainnet, set up with Tempo) on their overview pages. The earlier `/docs/guide/private-zones` sandbox is a separate integration. - `public/SKILL.md` is the canonical Tempo docs skill, published at `/SKILL.md`. The `tempoxyz/plugins` copy is generated from it by that repo's skill sync, so edit only `public/SKILL.md` here. - `src/lib/markdown-output-transform.ts` uses `src/lib/ai-docs.ts` to add section context before Vocs validates and writes Markdown exports. `SKILL.md` links to `public/SKILL.md`, the source Vite serves and copies. Never edit `dist` or `.vercel` copies by hand. diff --git a/public/SKILL.md b/public/SKILL.md index cc6f688da..31b8a64e0 100644 --- a/public/SKILL.md +++ b/public/SKILL.md @@ -16,23 +16,26 @@ All links below use the canonical docs mount at `https://tempo.xyz/developers`. | Section | Start here when the reader wants to… | | --- | --- | -| [Get Started](https://tempo.xyz/developers/get-started.md) | Choose an integration, send a first test payment, get testnet funds, connect a coding agent, or find the SDKs and CLI. | +| [Get Started](https://tempo.xyz/developers/get-started.md) | Choose an integration, send a first test payment, get testnet funds, or connect a coding agent. | | [Accounts](https://tempo.xyz/developers/docs/accounts.md) | Create accounts, set up sign-in, security policies, access keys, and fees, or follow guides for wallet providers, SSO, self-custody, agents, automations, spending, and treasuries. | | [Earn](https://tempo.xyz/developers/docs/earn.md) | Choose a vault, deposit stablecoins, read positions, or withdraw. Earn is in beta; check access on the introduction page. | | [Routes](https://tempo.xyz/developers/docs/routes.md) | Accept cross-network deposits, quote transfers, or track delivery. Routes is in beta; check access on the introduction page. | | [Zones](https://tempo.xyz/developers/docs/zones.md) | Connect to a Zone and work with private balances, deposits, or withdrawals. Zones is live on Mainnet; check how to set up a Zone on the introduction page. | | [Machine Payments](https://tempo.xyz/developers/docs/agents.md) | Pay for APIs, accept API payments, discover services, or integrate MPP. | -| [Tempo EVM](https://tempo.xyz/developers/docs/development.md) | Build wallet support, manage signing keys, send or receive payments and reconcile deposits, use transactions and fees, issue TIP-20 Tokens, deploy contracts, exchange stablecoins, run nodes, or read protocol specifications and changelog. | +| [Tempo EVM](https://tempo.xyz/developers/docs/development.md) | Build wallet support, manage signing keys, send or receive payments and reconcile deposits, use transactions and fees, issue TIP-20 Tokens, deploy contracts, exchange stablecoins, or read protocol specifications. | +| [Operate](https://tempo.xyz/developers/docs/guide/node.md) | Run RPC, standby, or validator nodes, upgrade node software, or follow network upgrades, activation timelines, and node releases. | | [API reference](https://tempo.xyz/developers/docs/api.md) | Find Tempo API endpoints by product, authentication, Console management endpoints, JSON-RPC, and the indexer. | +| [SDKs & CLI](https://tempo.xyz/developers/docs/sdk.md) | Install a Tempo SDK for TypeScript, Rust, Go, Python, or Foundry, use the Tempo API client or server handlers, or run the Tempo CLI. | +| [Guides](https://tempo.xyz/developers/docs/resources/guides.md) | Plan payment infrastructure and workflows, liquidity and issuance, machine payments, wallets and funding, or compliance controls. | | [Partners](https://tempo.xyz/developers/docs/partners.md) | Find third-party wallets, bridges, RPC infrastructure, data services, and integrations. | -Accounts covers account setup and its guides; Tempo EVM covers payments and the chain capabilities behind them. Routes covers cross-network movement. The API reference supplements product guides with exact endpoints and schemas. SDKs and the CLI are under Get Started. +Accounts covers account setup and its guides; Tempo EVM covers payments and the chain capabilities behind them. Routes covers cross-network movement. The API reference supplements product guides with exact endpoints and schemas. SDKs & CLI covers the client libraries and the Tempo CLI. Choose providers through the task guides for [wallets](https://tempo.xyz/developers/docs/resources/guides/embedded-wallets.md), [funding and payouts](https://tempo.xyz/developers/docs/resources/guides/funding-and-payouts.md), [payment screening](https://tempo.xyz/developers/docs/resources/guides/payment-screening.md), and [reconciliation](https://tempo.xyz/developers/docs/resources/guides/payment-reconciliation.md). Use [node providers](https://tempo.xyz/developers/docs/partners/rpc-and-nodes.md) for hosted RPC access. For application checkout and returns, use [payment links](https://tempo.xyz/developers/docs/resources/guides/payment-links.md) and [refunds](https://tempo.xyz/developers/docs/resources/guides/refunds.md). These workflows connect trusted request records and amount reservations to the maintained payment primitives. -Within Tempo EVM, node operators start at [Run a node](https://tempo.xyz/developers/docs/guide/node.md). For network upgrades and activation timelines, read [Changelog](https://tempo.xyz/developers/docs/protocol/upgrades.md); for node software versions, read [Node releases](https://tempo.xyz/developers/docs/changelog.md). +Within Operate, node operators start at [Run a node](https://tempo.xyz/developers/docs/guide/node.md). For network upgrades and activation timelines, read [Changelog](https://tempo.xyz/developers/docs/protocol/upgrades.md); for node software versions, read [Node releases](https://tempo.xyz/developers/docs/changelog.md). Use current Zones pages before the earlier `/docs/guide/private-zones` sandbox walkthroughs. That sandbox uses a separate integration and does not establish which operations are available in current Mainnet deployments. diff --git a/src/lib/ai-docs.test.ts b/src/lib/ai-docs.test.ts index 94f465767..b70429fd7 100644 --- a/src/lib/ai-docs.test.ts +++ b/src/lib/ai-docs.test.ts @@ -20,6 +20,9 @@ const input = `# Tempo Docs - [Machine Payments](/docs/agents): Pay for APIs. - [API reference](/docs/api/reference): REST schemas. - [Keys](/docs/accounts/keys): Chain signing permissions. +- [Node releases](/docs/changelog): Node software versions. +- [API client](/docs/sdk/typescript/api-client): Typed Tempo API calls. +- [Payment links](/docs/resources/guides/payment-links): Plan payment requests. - [Home](/index): Site home. ` @@ -43,8 +46,11 @@ describe('AI documentation navigation', () => { expect(output).toMatch( /## Zones[\s\S]*live on Mainnet[\s\S]*### Earlier testnet sandbox[\s\S]*Legacy swap/, ) - expect(output).toMatch(/## Tempo EVM[\s\S]*\[Keys\]/) + expect(output).toMatch(/## Tempo EVM[\s\S]*\[Keys\][\s\S]*## Operate/) + expect(output).toMatch(/## Operate[\s\S]*\[Node releases\][\s\S]*## API reference/) expect(output).toMatch(/## API reference[\s\S]*\[API reference\]/) + expect(output).toMatch(/## SDKs & CLI[\s\S]*\[API client\][\s\S]*## Guides/) + expect(output).toMatch(/## Guides[\s\S]*\[Payment links\][\s\S]*## Partners/) expect(output).toContain('Earn is in beta') expect(output).toContain('Routes is in beta') expect(renderAiIndex(output)).toBe(output) @@ -67,6 +73,9 @@ describe('AI documentation navigation', () => { expect(full).toContain('> Section: [Earn](/docs/earn.md)') expect(full).toContain('> Source: [/docs/zones/deposit](/docs/zones/deposit.md)') expect(full).toContain('> Section: [Zones](/docs/zones.md)') + expect(full).toContain('> Section: [Operate](/docs/guide/node.md)') + expect(full).toContain('> Section: [SDKs & CLI](/docs/sdk.md)') + expect(full).toContain('> Section: [Guides](/docs/resources/guides.md)') expect(() => renderAiFull(index, new Map())).toThrow('Missing Markdown export') }) diff --git a/src/lib/ai-docs.ts b/src/lib/ai-docs.ts index 576be888a..3a468cd47 100644 --- a/src/lib/ai-docs.ts +++ b/src/lib/ai-docs.ts @@ -11,7 +11,7 @@ export const aiDocsDescription = const sectionDescriptions: Partial> = { overview: - 'Choose a starting point, send a test payment, get testnet funds, connect a coding agent, find the SDKs and CLI, and use the resource guides to plan payment workflows, liquidity, provider selection, and partner integrations.', + 'Choose a starting point, send a test payment, get testnet funds, connect a coding agent, and explore stablecoins and use cases on Tempo.', accounts: 'Create accounts, set up signing methods, policies, access keys, key rotation, and fees, and follow guides for users, automations, agents, and teams.', earn: 'Choose a vault, deposit stablecoins, read positions, and withdraw. Earn is in beta; see the introduction for access.', @@ -22,8 +22,14 @@ const sectionDescriptions: Partial> = { 'machine-payments': 'Pay for APIs, accept API payments, find services, and use MPP (https://mpp.dev).', developers: - 'Build on Tempo EVM: wallets and keys, payments, transactions and fees, TIP-20 Tokens, contracts, exchange, network details, nodes, protocol specifications, and changelog.', + 'Build on Tempo EVM: wallets and keys, payments, transactions and fees, TIP-20 Tokens, contracts, exchange, network details, and protocol specifications.', + operate: + 'Run RPC, standby, and validator nodes, keep them secure and upgraded, and follow network upgrades, activation timelines, and node releases.', api: 'Tempo API reference: authentication, conventions, endpoints grouped by product, Console management endpoints, JSON-RPC, and the indexer. An API key authenticates API requests; an account signing key authorizes transactions.', + tools: + 'Use the Tempo SDKs for TypeScript, Rust, Go, Python, and Foundry, the Tempo API client, and server handlers, or the Tempo CLI for API calls, wallets, paid requests, snapshots, and nodes.', + resources: + 'Plan stablecoin payment infrastructure and workflows, liquidity and issuance, machine payment models, wallet and funding providers, and compliance controls.', ecosystem: 'Find wallet providers, bridges, RPC infrastructure, data services, and other third-party integrations.', } diff --git a/src/lib/docs-sections.ts b/src/lib/docs-sections.ts index 24c61f676..0af4a176e 100644 --- a/src/lib/docs-sections.ts +++ b/src/lib/docs-sections.ts @@ -7,8 +7,10 @@ export type DocsSection = { | 'zones' | 'developers' | 'machine-payments' + | 'operate' | 'api' | 'tools' + | 'resources' | 'ecosystem' label: string href: string @@ -23,16 +25,10 @@ export const docsSections: DocsSection[] = [ href: '/get-started', matches: [ '/get-started', - '/docs/resources', '/docs/quickstart/faucet', '/docs/guide/using-tempo-with-ai', - '/docs/tools', '/docs/developer-tools', '/docs/quickstart/developer-tools', - '/docs/sdk', - '/docs/cli', - '/docs/wallet', - '/docs/server', ], }, { @@ -69,13 +65,17 @@ export const docsSections: DocsSection[] = [ '/docs/development', '/docs/quickstart', '/docs/protocol', - '/docs/changelog', '/docs/guide/tempo-transaction', '/docs/guide/issuance', '/docs/guide/stablecoin-dex', - '/docs/guide/node', ], }, + { + id: 'operate', + label: 'Operate', + href: '/docs/guide/node', + matches: ['/docs/guide/node', '/docs/changelog', '/docs/protocol/upgrades'], + }, ] export const docsUtilitySections: DocsSection[] = [ @@ -85,6 +85,18 @@ export const docsUtilitySections: DocsSection[] = [ href: '/docs/api', matches: ['/docs/api'], }, + { + id: 'tools', + label: 'SDKs & CLI', + href: '/docs/sdk', + matches: ['/docs/tools', '/docs/sdk', '/docs/cli', '/docs/wallet', '/docs/server'], + }, + { + id: 'resources', + label: 'Guides', + href: '/docs/resources/guides', + matches: ['/docs/resources'], + }, { id: 'ecosystem', label: 'Partners', @@ -99,6 +111,60 @@ export const docsUtilitySections: DocsSection[] = [ }, ] +export type DocsArea = { + id: 'get-started' | 'products' | 'evm' | 'operate' | 'reference' | 'resources' + label: string + href: string + description: string + sections: DocsSection['id'][] +} + +/** The six areas in the global nav. Each groups one or more sections, which own the sidebars. */ +export const docsAreas: DocsArea[] = [ + { + id: 'get-started', + label: 'Get started', + href: '/get-started', + description: 'Quickstart, testnet funds and AI setup', + sections: ['overview'], + }, + { + id: 'products', + label: 'Products', + href: '/docs/accounts', + description: 'Accounts, Earn, Routes, Zones and Machine Payments', + sections: ['accounts', 'earn', 'routes', 'zones', 'machine-payments'], + }, + { + id: 'evm', + label: 'Tempo EVM', + href: '/docs/development', + description: 'Build directly on the chain', + sections: ['developers'], + }, + { + id: 'operate', + label: 'Operate', + href: '/docs/guide/node', + description: 'Run nodes and track network upgrades', + sections: ['operate'], + }, + { + id: 'reference', + label: 'Reference', + href: '/docs/api', + description: 'Tempo API, SDKs and CLI', + sections: ['api', 'tools'], + }, + { + id: 'resources', + label: 'Resources', + href: '/docs/resources/guides', + description: 'Payment guides and partners', + sections: ['resources', 'ecosystem'], + }, +] + export function normalizeDocsSectionPath(path: string) { return path.replace(/^\/developers(?=\/|$)/, '').replace(/\/+$/, '') || '/' } @@ -114,3 +180,15 @@ export function getActiveDocsSection(path: string) { .filter(({ prefix }) => matchesDocsSectionPath(pathname, prefix)) .sort((a, b) => b.prefix.length - a.prefix.length)[0]?.section } + +export function getDocsSectionById(id: DocsSection['id']) { + return [...docsSections, ...docsUtilitySections].find((section) => section.id === id) +} + +export function getDocsAreaForSection(id: DocsSection['id'] | undefined) { + return id ? docsAreas.find((area) => area.sections.includes(id)) : undefined +} + +export function getActiveDocsArea(path: string) { + return getDocsAreaForSection(getActiveDocsSection(path)?.id) +} diff --git a/src/lib/og-sections.test.ts b/src/lib/og-sections.test.ts index 58ff7ecc9..badd48295 100644 --- a/src/lib/og-sections.test.ts +++ b/src/lib/og-sections.test.ts @@ -1,6 +1,12 @@ import { describe, expect, it } from 'vitest' import { docsSections } from './docs-sections' -import { OG_IMAGE_VERSION, ogLandingPaths, ogSectionMap, ogSubsectionMap } from './og-sections' +import { + OG_IMAGE_VERSION, + ogLandingPaths, + ogSectionMap, + ogSectionPrefixMap, + ogSubsectionMap, +} from './og-sections' // vocs.config.ts's `ogImageUrl` is serialized to source and re-evaluated at // runtime, so it cannot import the maps in og-sections.ts — it carries an @@ -27,7 +33,7 @@ function parseInlineMap(source: string, name: string): Record { expect(match, `inline ${name} in vocs.config.ts ogImageUrl`).toBeTruthy() const map: Record = {} for (const [, key, value] of (match as RegExpMatchArray)[1].matchAll( - /["']?([\w-]+)["']?\s*:\s*["']([^"']*)["']/g, + /["']?([\w/-]+)["']?\s*:\s*["']([^"']*)["']/g, )) { map[key] = value } @@ -45,6 +51,10 @@ describe('vocs.config ogImageUrl stays in sync with src/lib/og-sections', () => expect(parseInlineMap(source, 'subsectionMap')).toEqual(ogSubsectionMap) }) + it('has identical section prefix maps (both directions)', () => { + expect(parseInlineMap(source, 'sectionPrefixMap')).toEqual(ogSectionPrefixMap) + }) + it('has identical landing paths', () => { const match = source.match(/const landingPaths\s*=\s*\[([^\]]*)\]/) expect(match).toBeTruthy() @@ -64,6 +74,11 @@ describe('vocs.config ogImageUrl stays in sync with src/lib/og-sections', () => expect(param(url, 'section')).toBe(label) }) + it.each(Object.entries(ogSectionPrefixMap))('maps pages under %s -> %s', (prefix, label) => { + expect(param(ogImageUrl(`/docs${prefix}`), 'section')).toBe(label) + expect(param(ogImageUrl(`/docs${prefix}/some-page`), 'section')).toBe(label) + }) + it.each(docsSections)('uses the current $label product name on its social card', (section) => { expect(param(ogImageUrl(section.href), 'section')).toBe(section.label.toUpperCase()) }) diff --git a/src/lib/og-sections.ts b/src/lib/og-sections.ts index 9fe1a0e4c..11643823c 100644 --- a/src/lib/og-sections.ts +++ b/src/lib/og-sections.ts @@ -41,6 +41,12 @@ export const ogSectionMap: Record = { zones: 'ZONES', } +/** Route prefix → section label, checked before `ogSectionMap`. Operate's pages live under `guide` and `protocol`. */ +export const ogSectionPrefixMap: Record = { + '/guide/node': 'OPERATE', + '/protocol/upgrades': 'OPERATE', +} + /** Second path segment → subsection label (only applied to 3+ segment routes). */ export const ogSubsectionMap: Record = { blockspace: 'BLOCKSPACE', diff --git a/src/lib/sidebar-coverage.test.ts b/src/lib/sidebar-coverage.test.ts index 002fcc3c8..76a489ded 100644 --- a/src/lib/sidebar-coverage.test.ts +++ b/src/lib/sidebar-coverage.test.ts @@ -27,9 +27,6 @@ const generatedApiGroups = new Set([ ]) // Supporting guides remain discoverable through their primary guide or partner category. const supportingPages = new Map([ - ['/docs/guide/bridge-layerzero', '/docs/partners/bridges'], - ['/docs/guide/bridge-bungee', '/docs/partners/bridges'], - ['/docs/guide/bridge-relay', '/docs/partners/bridges'], ['/docs/partners/developer-tools', '/docs/partners/explorers-and-debugging'], ['/docs/guide/payments/send-a-payment/examples', '/docs/guide/payments/send-a-payment'], ]) @@ -74,29 +71,60 @@ describe('documentation sidebar coverage', () => { }) }) +describe('section sidebars', () => { + it.each([...docsSections, ...docsUtilitySections])('$label sidebar links its landing page', ({ + href, + }) => { + expect(links(sidebarFor(href))).toContain(href) + }) + + it('keeps Get Started to onboarding pages', () => { + type SidebarNode = { text?: string; items?: SidebarNode[] } + const sidebar = sidebarFor('/get-started') as SidebarNode[] + expect(sidebar.map(({ text }) => text)).toEqual(['Get Started']) + }) + + it.each([ + ['/docs/guide/node/validator-keys', 'operate', '/docs/guide/node'], + ['/docs/guide/node/network-upgrades', 'operate', '/docs/guide/node'], + ['/docs/protocol/upgrades/t11', 'operate', '/docs/guide/node'], + ['/docs/changelog', 'operate', '/docs/guide/node'], + ['/docs/cli/api', 'tools', '/docs/sdk'], + ['/docs/wallet/recipes', 'tools', '/docs/sdk'], + ['/docs/sdk/typescript/api-client', 'tools', '/docs/sdk'], + ['/docs/server/relay-handler', 'tools', '/docs/sdk'], + ['/docs/resources/guides/payment-links', 'resources', '/docs/resources/guides'], + ['/docs/guide/bridge-relay', 'ecosystem', '/docs/partners'], + ])('%s appears once, in the %s sidebar', (route, section, landing) => { + expect(getActiveDocsSection(route)?.id).toBe(section) + expect(sidebarFor(route)).toBe(sidebarFor(landing)) + const sidebars = [...new Set(Object.values(config.sidebar as Record))] + expect(sidebars.filter((sidebar) => links(sidebar).includes(route))).toEqual([ + sidebarFor(landing), + ]) + expect(links(sidebarFor(route)).filter((link) => link === route)).toHaveLength(1) + }) +}) + describe('resource guide navigation', () => { - it('keeps guides in a collapsed submenu below the main onboarding content', () => { + it('lists guides in their own sidebar with collapsed topics', () => { type SidebarNode = { text?: string; link?: string; collapsed?: boolean; items?: SidebarNode[] } - const sidebar = sidebarFor('/get-started') as SidebarNode[] - const resources = sidebar.at(-1) - expect(resources?.text).toBe('Resources') - expect(resources?.items?.[0]).toMatchObject({ text: 'Guides', collapsed: true }) - const guides = resources?.items?.[0] - expect(resources?.items?.find(({ text }) => text === 'Partner guides')).toBeUndefined() - expect(guides?.items?.filter(({ items }) => items).every(({ collapsed }) => collapsed)).toBe( + const sidebar = sidebarFor('/docs/resources/guides') as SidebarNode[] + expect(sidebar.map(({ text }) => text)).toEqual(['Guides']) + const guides = sidebar[0] + expect(guides.items?.[0]).toEqual({ text: 'All guides', link: '/docs/resources/guides' }) + expect(guides.items?.filter(({ items }) => items).every(({ collapsed }) => collapsed)).toBe( true, ) - const compliance = guides?.items?.find(({ text }) => text === 'Compliance and security') + const compliance = guides.items?.find(({ text }) => text === 'Compliance and security') expect(links(compliance)).toContain('/docs/resources/guides/payment-screening') expect(links(compliance)).toContain('/docs/resources/guides/compliance-controls') - expect(links(resources)).toContain('/docs/resources/guides') - expect(sidebar[0].items?.some(({ text }) => text === 'Guides')).toBe(false) }) - it('assigns resource guides to Get Started without a new primary section', () => { + it('assigns resource guides to the Guides section', () => { for (const route of routes.filter((route) => route.startsWith('/docs/resources/'))) { - expect(getActiveDocsSection(route)?.id).toBe('overview') - expect(sidebarFor(route)).toBe(sidebarFor('/get-started')) + expect(getActiveDocsSection(route)?.id).toBe('resources') + expect(sidebarFor(route)).toBe(sidebarFor('/docs/resources/guides')) } }) }) diff --git a/vocs.config.ts b/vocs.config.ts index 25041a346..d87cbcac4 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -348,10 +348,22 @@ export default defineConfig({ zones: 'ZONES', } + // Operate owns node and upgrade pages that live under the guide and protocol paths. + const sectionPrefixMap: Record = { + '/guide/node': 'OPERATE', + '/protocol/upgrades': 'OPERATE', + } + const segments = docsPath.split('/').filter(Boolean) const firstSeg = segments[0] || '' const secondSeg = segments[1] || '' - const section = sectionMap[firstSeg] || firstSeg.toUpperCase().replace(/-/g, ' ') + const sectionPrefix = Object.keys(sectionPrefixMap).find( + (prefix) => docsPath === prefix || docsPath.startsWith(`${prefix}/`), + ) + const section = + (sectionPrefix && sectionPrefixMap[sectionPrefix]) || + sectionMap[firstSeg] || + firstSeg.toUpperCase().replace(/-/g, ' ') const subsection = segments.length >= 3 && subsectionMap[secondSeg] ? subsectionMap[secondSeg] @@ -382,7 +394,7 @@ export default defineConfig({ groupItems: { 'JSON-RPC': [{ text: 'Guide', link: '/docs/api/json-rpc' }], Indexer: [{ text: 'Guide', link: '/docs/api/indexer-api' }], - 'API keys': [{ text: 'Guide', link: '/docs/api/console/api-keys' }], + 'API keys': [{ text: 'Create and manage API keys', link: '/docs/api/console/api-keys' }], Projects: [{ text: 'Guide', link: '/docs/api/console/projects-and-environments' }], Team: [{ text: 'Guide', link: '/docs/api/console/team' }], 'Billing and usage': [{ text: 'Guide', link: '/docs/api/console/usage-and-billing' }], @@ -457,24 +469,8 @@ export default defineConfig({ { text: 'Use cases', link: '/get-started/use-cases' }, ], }, - { - text: 'Tempo CLI', - items: [ - { text: 'Overview', link: '/docs/cli' }, - { text: 'API', link: '/docs/cli/api' }, - { - text: 'Wallet', - collapsed: true, - items: [ - { text: 'Overview', link: '/docs/cli/wallet' }, - { text: 'Recipes', link: '/docs/wallet/recipes' }, - ], - }, - { text: 'Request', link: '/docs/cli/request' }, - { text: 'Download', link: '/docs/cli/download' }, - { text: 'Node', link: '/docs/cli/node' }, - ], - }, + ] + const toolsSidebar = [ { text: 'Tempo SDK', items: [ @@ -513,127 +509,142 @@ export default defineConfig({ ], }, { - text: 'Resources', + text: 'Tempo CLI', items: [ + { text: 'Overview', link: '/docs/cli' }, + { text: 'API', link: '/docs/cli/api' }, { - text: 'Guides', + text: 'Wallet', + collapsed: true, + items: [ + { text: 'Overview', link: '/docs/cli/wallet' }, + { text: 'Recipes', link: '/docs/wallet/recipes' }, + ], + }, + { text: 'Request', link: '/docs/cli/request' }, + { text: 'Download', link: '/docs/cli/download' }, + { text: 'Node', link: '/docs/cli/node' }, + ], + }, + { text: 'Explorer', link: 'https://explore.tempo.xyz' }, + ] + const resourcesSidebar = [ + { + text: 'Guides', + items: [ + { text: 'All guides', link: '/docs/resources/guides' }, + { + text: 'Payment infrastructure', collapsed: true, items: [ - { text: 'All guides', link: '/docs/resources/guides' }, { - text: 'Payment infrastructure', - collapsed: true, - items: [ - { - text: 'Blockchain payment flows', - link: '/docs/resources/guides/blockchain-payments', - }, - { - text: 'Stablecoins for business', - link: '/docs/resources/guides/stablecoins-for-business', - }, - { - text: 'Plan a payment stack', - link: '/docs/resources/guides/stablecoin-payment-infrastructure-enterprise-scale', - }, - { - text: 'Stablecoin settlement', - link: '/docs/resources/guides/stablecoin-settlement', - }, - { text: 'Compare payment rails', link: '/docs/resources/guides/payment-rails' }, - { text: 'Estimate payment fees', link: '/docs/resources/guides/stablecoin-fees' }, - ], + text: 'Blockchain payment flows', + link: '/docs/resources/guides/blockchain-payments', }, { - text: 'Payment workflows', - collapsed: true, - items: [ - { - text: 'Accept stablecoin payments', - link: '/docs/resources/guides/how-to-accept-stablecoin-payments', - }, - { text: 'Create payment links', link: '/docs/resources/guides/payment-links' }, - { text: 'Handle refunds', link: '/docs/resources/guides/refunds' }, - { - text: 'Reconcile payments', - link: '/docs/resources/guides/payment-reconciliation', - }, - { text: 'Plan batch payments', link: '/docs/resources/guides/batch-payments' }, - { text: 'Global payouts', link: '/docs/resources/guides/global-payouts' }, - { - text: 'Global payroll', - link: '/docs/resources/guides/stablecoin-payroll-for-global-teams', - }, - { - text: 'Cross-border payments', - link: '/docs/resources/guides/cross-border-payments', - }, - { - text: 'Latin America payment routes', - link: '/docs/resources/guides/cross-border-payments-latin-america', - }, - { text: 'Remittance operations', link: '/docs/resources/guides/remittances' }, - { - text: 'Recurring payments', - link: '/docs/resources/guides/subscriptions-on-tempo', - }, - ], + text: 'Stablecoins for business', + link: '/docs/resources/guides/stablecoins-for-business', }, { - text: 'Liquidity and issuance', - collapsed: true, - items: [ - { text: 'Corporate treasury', link: '/docs/resources/guides/corporate-treasury' }, - { - text: 'Nostro and vostro accounts', - link: '/docs/resources/guides/nostro-vostro-accounts', - }, - { text: 'Plan prefunding', link: '/docs/resources/guides/prefunding' }, - { text: 'FBO accounts', link: '/docs/resources/guides/fbo-accounts' }, - { text: 'The GENIUS Act', link: '/docs/resources/guides/genius-act' }, - ], + text: 'Plan a payment stack', + link: '/docs/resources/guides/stablecoin-payment-infrastructure-enterprise-scale', }, { - text: 'Machine payment models', - collapsed: true, - items: [ - { - text: 'HTTP 402 payment flows', - link: '/docs/resources/guides/402-payment-required', - }, - { - text: 'Microtransactions and sessions', - link: '/docs/resources/guides/microtransactions', - }, - ], + text: 'Stablecoin settlement', + link: '/docs/resources/guides/stablecoin-settlement', }, + { text: 'Compare payment rails', link: '/docs/resources/guides/payment-rails' }, + { text: 'Estimate payment fees', link: '/docs/resources/guides/stablecoin-fees' }, + ], + }, + { + text: 'Payment workflows', + collapsed: true, + items: [ { - text: 'Wallets and funding', - collapsed: true, - items: [ - { - text: 'Choose embedded wallets', - link: '/docs/resources/guides/embedded-wallets', - }, - { - text: 'Choose funding and payouts', - link: '/docs/resources/guides/funding-and-payouts', - }, - ], + text: 'Accept stablecoin payments', + link: '/docs/resources/guides/how-to-accept-stablecoin-payments', }, + { text: 'Create payment links', link: '/docs/resources/guides/payment-links' }, + { text: 'Handle refunds', link: '/docs/resources/guides/refunds' }, { - text: 'Compliance and security', - collapsed: true, - items: [ - { - text: 'Screen blockchain payments', - link: '/docs/resources/guides/payment-screening', - }, - { - text: 'Design payment controls', - link: '/docs/resources/guides/compliance-controls', - }, - ], + text: 'Reconcile payments', + link: '/docs/resources/guides/payment-reconciliation', + }, + { text: 'Plan batch payments', link: '/docs/resources/guides/batch-payments' }, + { text: 'Global payouts', link: '/docs/resources/guides/global-payouts' }, + { + text: 'Global payroll', + link: '/docs/resources/guides/stablecoin-payroll-for-global-teams', + }, + { + text: 'Cross-border payments', + link: '/docs/resources/guides/cross-border-payments', + }, + { + text: 'Latin America payment routes', + link: '/docs/resources/guides/cross-border-payments-latin-america', + }, + { text: 'Remittance operations', link: '/docs/resources/guides/remittances' }, + { + text: 'Recurring payments', + link: '/docs/resources/guides/subscriptions-on-tempo', + }, + ], + }, + { + text: 'Liquidity and issuance', + collapsed: true, + items: [ + { text: 'Corporate treasury', link: '/docs/resources/guides/corporate-treasury' }, + { + text: 'Nostro and vostro accounts', + link: '/docs/resources/guides/nostro-vostro-accounts', + }, + { text: 'Plan prefunding', link: '/docs/resources/guides/prefunding' }, + { text: 'FBO accounts', link: '/docs/resources/guides/fbo-accounts' }, + { text: 'The GENIUS Act', link: '/docs/resources/guides/genius-act' }, + ], + }, + { + text: 'Machine payment models', + collapsed: true, + items: [ + { + text: 'HTTP 402 payment flows', + link: '/docs/resources/guides/402-payment-required', + }, + { + text: 'Microtransactions and sessions', + link: '/docs/resources/guides/microtransactions', + }, + ], + }, + { + text: 'Wallets and funding', + collapsed: true, + items: [ + { + text: 'Choose embedded wallets', + link: '/docs/resources/guides/embedded-wallets', + }, + { + text: 'Choose funding and payouts', + link: '/docs/resources/guides/funding-and-payouts', + }, + ], + }, + { + text: 'Compliance and security', + collapsed: true, + items: [ + { + text: 'Screen blockchain payments', + link: '/docs/resources/guides/payment-screening', + }, + { + text: 'Design payment controls', + link: '/docs/resources/guides/compliance-controls', }, ], }, @@ -906,7 +917,16 @@ export default defineConfig({ { text: 'Wallets', link: '/docs/partners/wallets' }, { text: 'Smart accounts', link: '/docs/partners/smart-accounts' }, { text: 'Exchanges', link: '/docs/partners/exchanges' }, - { text: 'Bridges', link: '/docs/partners/bridges' }, + { + text: 'Bridges', + collapsed: false, + items: [ + { text: 'Overview', link: '/docs/partners/bridges' }, + { text: 'LayerZero', link: '/docs/guide/bridge-layerzero' }, + { text: 'Relay', link: '/docs/guide/bridge-relay' }, + { text: 'Bungee', link: '/docs/guide/bridge-bungee' }, + ], + }, { text: 'Payments & ramps', link: '/docs/partners/payments-and-ramps' }, { text: 'Cards', link: '/docs/partners/cards' }, { text: 'Partner with Tempo', link: '/docs/partners/join' }, @@ -924,7 +944,6 @@ export default defineConfig({ ] const nodeSidebar = { text: 'Run a node', - collapsed: true, items: [ { text: 'Overview', @@ -948,6 +967,7 @@ export default defineConfig({ }, { text: 'Run a validator', + collapsed: true, items: [ { text: 'Overview', @@ -1001,138 +1021,75 @@ export default defineConfig({ }, ], } - const changelogSidebar = [ - { text: '← Back to Tempo EVM', link: '/docs/development' }, - { - text: 'Changelog', - items: [ - { text: 'Overview', link: '/docs/protocol/upgrades' }, - { - text: 'Network upgrades', - collapsed: false, - items: [ - { - text: 'T14', - badge: { text: 'Planned', variant: 'note' as const }, - link: '/docs/protocol/upgrades/t14', - }, - { - text: 'T13', - badge: { text: 'Mainnet', variant: 'info' as const }, - link: '/docs/protocol/upgrades/t13', - }, - { - text: 'T12', - link: '/docs/protocol/upgrades/t12', - }, - { - text: 'T11', - link: '/docs/protocol/upgrades/t11', - }, - { - text: 'T10', - link: '/docs/protocol/upgrades/t10', - }, - { - text: 'T9', - link: '/docs/protocol/upgrades/t9', - }, - { - text: 'T8', - link: '/docs/protocol/upgrades/t8', - }, - { - text: 'T7', - link: '/docs/protocol/upgrades/t7', - }, - { - text: 'T6', - link: '/docs/protocol/upgrades/t6', - }, - { - text: 'T5', - link: '/docs/protocol/upgrades/t5', - }, - { - text: 'T4', - link: '/docs/protocol/upgrades/t4', - }, - { - text: 'T3', - link: '/docs/protocol/upgrades/t3', - }, - { - text: 'T2', - link: '/docs/protocol/upgrades/t2', - }, - ], - }, - { text: 'Node releases', link: '/docs/changelog' }, - { text: 'Operator updates', link: '/docs/guide/node/network-upgrades' }, - ], - }, - ] - const developerToolsSidebar = [ - { - text: 'SDKs & CLI', - items: [ - { - text: 'Overview', - link: '/docs/sdk', - }, - { - text: 'SDKs', - collapsed: false, - items: [ - { text: 'Overview', link: '/docs/sdk' }, - { - text: 'TypeScript', - collapsed: false, - items: [ - { text: 'Overview', link: '/docs/sdk/typescript' }, - { text: 'Local testing with Prool', link: '/docs/sdk/typescript/prool/setup' }, - { text: 'Viem reference', link: 'https://viem.sh/tempo' }, - { text: 'Wagmi reference', link: 'https://wagmi.sh/tempo' }, - ], - }, - { text: 'Rust', link: '/docs/sdk/rust' }, - { text: 'Go', link: '/docs/sdk/go' }, - { text: 'Python', link: '/docs/sdk/python' }, - { - text: 'Foundry', - collapsed: false, - items: [ - { text: 'Overview', link: '/docs/sdk/foundry' }, - { text: 'Use MPP with Foundry', link: '/docs/sdk/foundry/mpp' }, - { text: 'Signature verification', link: '/docs/sdk/foundry/signature-verifier' }, - ], - }, - ], - }, - { - text: 'CLI', - collapsed: false, - items: [ - { text: 'Overview', link: '/docs/cli' }, - { text: 'Wallet', link: '/docs/cli/wallet' }, - { text: 'Wallet recipes', link: '/docs/wallet/recipes' }, - { text: 'Request', link: '/docs/cli/request' }, - { text: 'Download', link: '/docs/cli/download' }, - { text: 'Node', link: '/docs/cli/node' }, - ], - }, - { - text: 'Server handlers', - collapsed: false, - items: [ - { text: 'Overview', link: '/docs/server' }, - { text: 'Relay and fee payer handler', link: '/docs/server/relay-handler' }, - ], - }, - { text: 'Explorer', link: 'https://explore.tempo.xyz' }, - ], - }, - ] + const changelogSidebar = { + text: 'Changelog', + items: [ + { text: 'Overview', link: '/docs/protocol/upgrades' }, + { + text: 'Network upgrades', + collapsed: false, + items: [ + { + text: 'T14', + badge: { text: 'Planned', variant: 'note' as const }, + link: '/docs/protocol/upgrades/t14', + }, + { + text: 'T13', + badge: { text: 'Mainnet', variant: 'info' as const }, + link: '/docs/protocol/upgrades/t13', + }, + { + text: 'T12', + link: '/docs/protocol/upgrades/t12', + }, + { + text: 'T11', + link: '/docs/protocol/upgrades/t11', + }, + { + text: 'T10', + link: '/docs/protocol/upgrades/t10', + }, + { + text: 'T9', + link: '/docs/protocol/upgrades/t9', + }, + { + text: 'T8', + link: '/docs/protocol/upgrades/t8', + }, + { + text: 'T7', + link: '/docs/protocol/upgrades/t7', + }, + { + text: 'T6', + link: '/docs/protocol/upgrades/t6', + }, + { + text: 'T5', + link: '/docs/protocol/upgrades/t5', + }, + { + text: 'T4', + link: '/docs/protocol/upgrades/t4', + }, + { + text: 'T3', + link: '/docs/protocol/upgrades/t3', + }, + { + text: 'T2', + link: '/docs/protocol/upgrades/t2', + }, + ], + }, + { text: 'Node releases', link: '/docs/changelog' }, + { text: 'Operator updates', link: '/docs/guide/node/network-upgrades' }, + ], + } + const operateSidebar = [nodeSidebar, changelogSidebar] // Keep guides and specifications in one chapter per topic. Group controls // disclose pages; Vocs opens the active chapter without replacing the tree. @@ -1293,7 +1250,7 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Introduction', link: '/docs/network' }, - { text: 'Connection details', link: '/docs/quickstart/connection-details' }, + { text: 'Network details', link: '/docs/quickstart/connection-details' }, { text: 'EVM differences', link: '/docs/quickstart/evm-compatibility' }, { text: 'Integrate an existing app', link: '/docs/quickstart/integrate-tempo' }, { text: 'JSON-RPC reference', link: '/docs/protocol/rpc' }, @@ -1306,21 +1263,10 @@ export default defineConfig({ { text: 'TIPs', link: 'https://tips.sh/' }, ], }, - { - text: 'Run a node', - collapsed: true, - items: nodeSidebar.items.map((item) => ({ - ...item, - ...(item.text === 'Overview' ? { text: 'Introduction' } : {}), - ...(item.items ? { collapsed: true } : {}), - })), - }, { text: 'All specifications', link: '/docs/protocol' }, - { ...changelogSidebar[1], collapsed: true }, ], }, ] - const toolsSidebar = developerToolsSidebar const partnerResourcesSidebar = [...ecosystemSidebar] const productSidebars = { overview: getStartedSidebar, @@ -1330,8 +1276,10 @@ export default defineConfig({ zones: zonesSidebar, developers: developersSidebar, 'machine-payments': machinePaymentsSidebar, + operate: operateSidebar, api: [{ text: 'API reference', items: [{ text: 'Introduction', link: '/docs/api' }] }], tools: toolsSidebar, + resources: resourcesSidebar, ecosystem: partnerResourcesSidebar, } From da8e1fd2935115bcb2ab0ce51f91ef17921d4ab7 Mon Sep 17 00:00:00 2001 From: Chase Gray Date: Tue, 10 Nov 2026 12:55:07 -0800 Subject: [PATCH 02/20] feat(nav): six-area global nav with product switcher and get api key One rounded bar holds the Tempo product switcher, the six areas (Products, Reference and Resources open menus), search, Agent setup, Log in and Get API key. The second row lists the sections of the current area. The mobile menu groups the six areas and keeps the external arrow for external links only. Co-Authored-By: Claude Opus 5.5 --- src/components/DocsHeader.test.ts | 50 ++- src/components/DocsHeader.tsx | 168 +++++--- src/components/DocsNavigation.styles.ts | 540 +++++++++++++++--------- src/components/DocsSectionNav.tsx | 312 +++++++++----- src/components/DocsSidebarDrawer.tsx | 3 +- 5 files changed, 684 insertions(+), 389 deletions(-) diff --git a/src/components/DocsHeader.test.ts b/src/components/DocsHeader.test.ts index 662321e8a..be6633643 100644 --- a/src/components/DocsHeader.test.ts +++ b/src/components/DocsHeader.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest' -import { docsSections, getActiveDocsSection } from '../lib/docs-sections' +import { + docsAreas, + docsSections, + getActiveDocsArea, + getActiveDocsSection, +} from '../lib/docs-sections' import { normalizeDocsPath, resolveSidebarItems } from '../lib/docs-sidebar' const sidebar = { @@ -41,7 +46,7 @@ describe('resolveSidebarItems', () => { }) describe('getActiveDocsSection', () => { - it('presents seven product and developer tabs without availability badges', () => { + it('groups the sections into six global nav areas', () => { expect(docsSections.map(({ label }) => label)).toEqual([ 'Get Started', 'Accounts', @@ -50,7 +55,20 @@ describe('getActiveDocsSection', () => { 'Zones', 'Machine Payments', 'Tempo EVM', + 'Operate', ]) + expect(docsAreas.map(({ label }) => label)).toEqual([ + 'Get started', + 'Products', + 'Tempo EVM', + 'Operate', + 'Reference', + 'Resources', + ]) + expect(getActiveDocsArea('/docs/earn/balances')?.id).toBe('products') + expect(getActiveDocsArea('/docs/cli/request')?.id).toBe('reference') + expect(getActiveDocsArea('/docs/partners/wallets')?.id).toBe('resources') + expect(getActiveDocsArea('/')).toBeUndefined() }) it('opens the guide library from Tempo EVM and the MPP introduction from Machine payments', () => { @@ -61,7 +79,7 @@ describe('getActiveDocsSection', () => { it('keeps section navigation active under the developers mount and trailing slashes', () => { expect(getActiveDocsSection('/developers/docs/api/transfers/')?.id).toBe('api') expect(getActiveDocsSection('/developers/get-started/')?.id).toBe('overview') - expect(getActiveDocsSection('/developers/docs/protocol/upgrades/t12/')?.id).toBe('developers') + expect(getActiveDocsSection('/developers/docs/protocol/upgrades/t12/')?.id).toBe('operate') expect(getActiveDocsSection('/developers/docs/protocol/')?.label).toBe('Tempo EVM') }) @@ -141,21 +159,23 @@ describe('getActiveDocsSection', () => { ['/docs/protocol/tip403/spec', 'developers'], ['/docs/protocol/exchange/spec', 'developers'], ['/docs/protocol/blockspace/payment-lane-specification', 'developers'], - ['/docs/guide/node/installation', 'developers'], - ['/docs/protocol/upgrades', 'developers'], - ['/docs/protocol/upgrades/t11', 'developers'], - ['/docs/protocol/upgrades/t12', 'developers'], - ['/docs/guide/node/upgrade-cadence', 'developers'], - ['/docs/guide/node/network-upgrades', 'developers'], - ['/docs/changelog', 'developers'], + ['/docs/guide/node/installation', 'operate'], + ['/docs/protocol/upgrades', 'operate'], + ['/docs/protocol/upgrades/t11', 'operate'], + ['/docs/protocol/upgrades/t12', 'operate'], + ['/docs/guide/node/upgrade-cadence', 'operate'], + ['/docs/guide/node/network-upgrades', 'operate'], + ['/docs/changelog', 'operate'], ['/docs/api/indexer-api', 'api'], ['/docs/protocol/rpc/eth_getBalance', 'developers'], - ['/docs/sdk/typescript', 'overview'], - ['/docs/cli/request', 'overview'], - ['/docs/wallet/recipes', 'overview'], - ['/docs/server/relay-handler', 'overview'], - ['/docs/tools', 'overview'], + ['/docs/sdk/typescript', 'tools'], + ['/docs/cli/request', 'tools'], + ['/docs/wallet/recipes', 'tools'], + ['/docs/server/relay-handler', 'tools'], + ['/docs/tools', 'tools'], ['/docs/quickstart/developer-tools', 'overview'], + ['/docs/resources/guides', 'resources'], + ['/docs/resources/guides/payroll', 'resources'], ])('assigns %s to the %s section', (pathname, expected) => { expect(getActiveDocsSection(pathname)?.id).toBe(expected) expect(getActiveDocsSection(`/developers${pathname}/`)?.id).toBe(expected) diff --git a/src/components/DocsHeader.tsx b/src/components/DocsHeader.tsx index f1a5f9e74..40c725308 100644 --- a/src/components/DocsHeader.tsx +++ b/src/components/DocsHeader.tsx @@ -11,6 +11,7 @@ import { normalizeDocsPath, type SidebarNode } from '../lib/docs-sidebar' export { normalizeDocsPath, resolveSidebarItems } from '../lib/docs-sidebar' import { DOCS_SEARCH_PARAM } from '../lib/docs-search' +import { docsAreas } from '../lib/docs-sections' import { publicAssetPath } from '../lib/public-asset-path' import { navActiveSquare } from '../styles/surfaces.styles' import { AmpLogo, ClaudeLogo, CodexLogo } from './AgentLogos' @@ -18,18 +19,28 @@ import { AgentSetupCommand } from './AgentSetupCommand' import { CopyIconSwap } from './CopyIconSwap' import * as ui from './DocsHeader.recipes' import { + docsAreaMenuItem, + docsAreaMenuItemDescription, + docsAreaMenuItemLabel, docsHeaderAccountEmail, docsHeaderActions, docsHeaderAgentMenu, docsHeaderAgentPanel, docsHeaderAgentTrigger, docsHeaderBrand, + docsHeaderDocsLabel, + docsHeaderGetKey, docsHeaderIconButton, - docsHeaderLoginPrimary, + docsHeaderKbd, + docsHeaderLeft, + docsHeaderLoginText, docsHeaderLogo, docsHeaderMenuButton, + docsHeaderMobileAccount, docsHeaderMobileActions, docsHeaderMobileAgents, + docsHeaderMobileAreaGroup, + docsHeaderMobileAreaLabel, docsHeaderMobileBody, docsHeaderMobileDialog, docsHeaderMobileLabel, @@ -38,20 +49,22 @@ import { docsHeaderMobileSections, docsHeaderMobileTheme, docsHeaderMobileTop, - docsHeaderMobileUtilityLink, docsHeaderMobileWebsite, docsHeaderNav, + docsHeaderSearchField, + docsHeaderSearchLabel, + docsHeaderSwitcher, docsSiteHeader, } from './DocsNavigation.styles' import { - DocsApiDropdown, + areaSections, + DocsAreaNav, + DocsNavMenu, DocsResourceLinks, - docsSections, - docsUtilitySections, + docsExternalTools, getActiveDocsSection, } from './DocsSectionNav' import { HeadingAnchorFeedback } from './HeadingAnchorFeedback' -import { iconButton } from './IconButton.recipes' import { SearchDialogMotion } from './SearchDialogMotion' import { SmoothCorners } from './SmoothCorners' @@ -844,6 +857,9 @@ export default function DocsHeader() { className={`docs-header-logo ${docsHeaderLogo().className}`} > + ) @@ -857,8 +873,29 @@ export default function DocsHeader() { className={`docs-header-nav ${docsHeaderNav().className}`} aria-label="Developer navigation" > - {brand} +
+ {brand} +
+ + {(closeMenu) => } + +
+ +
+
{email ? ( @@ -905,17 +938,11 @@ export default function DocsHeader() { 'Log in' )} - + {!email && ( + + Get API key + + )}

- Resources + Tempo products

- {docsUtilitySections.map((section) => - section.id === 'tools' ? ( - - ) : ( - - {section.label} - - - ), - )}
@@ -1027,12 +1051,42 @@ export default function DocsHeader() { > Back to tempo.xyz +
) } +/** The product switcher beside the logo: tempo.xyz and the other Tempo products. */ +function ProductSwitcherLinks({ onNavigate }: { onNavigate: () => void }) { + return ( + <> + {[{ label: 'tempo.xyz', href: 'https://tempo.xyz/' }, ...docsExternalTools].map((item) => ( + + + {item.label} + + {item.href.replace(/^https:\/\//, '').replace(/\/$/, '')} + + + + + ))} + + ) +} + function ThemeSelect() { const [theme, setTheme] = useState('system') useEffect(() => { diff --git a/src/components/DocsNavigation.styles.ts b/src/components/DocsNavigation.styles.ts index b90fa2516..b2a7d9875 100644 --- a/src/components/DocsNavigation.styles.ts +++ b/src/components/DocsNavigation.styles.ts @@ -28,12 +28,16 @@ export const docsSiteHeader = style({ }, }) +/** + * The global nav row. From Desktop up it sits in a rounded pill, like the tempo.xyz nav: + * brand and areas on the left, search and account actions on the right. + */ export const docsHeaderNav = style({ position: 'relative', zIndex: tokens.zIndex.header, - display: 'grid', - gridTemplateColumns: 'minmax(0, 1fr) auto', + display: 'flex', alignItems: 'center', + justifyContent: 'space-between', gap: tokens.spacing['2'], height: 'var(--tempo-docs-primary-nav-height)', paddingInline: tokens.spacing['5'], @@ -41,27 +45,297 @@ export const docsHeaderNav = style({ paddingInline: tokens.spacing['3_5'], }, '@media (width >= 1080px)': { - gap: tokens.spacing['5'], - paddingInline: tokens.spacing['7'], + gap: tokens.spacing['4'], + height: 'calc(var(--tempo-docs-primary-nav-height) - 16px)', + marginBlock: tokens.spacing['2'], + marginInline: tokens.spacing['4'], + paddingInline: tokens.spacing['3'], + borderRadius: tokens.radius.xl, + '--corner-radius': tokens.radius.xl, + backgroundColor: tokens.color.container, }, }) +export const docsHeaderLeft = style({ + display: 'flex', + alignItems: 'center', + gap: tokens.spacing['3'], + minWidth: 0, +}) + export const docsHeaderBrand = style({ display: 'flex', alignItems: 'center', - gap: tokens.spacing['7'], + gap: tokens.spacing['1'], minWidth: 0, whiteSpace: 'nowrap', - '@media (width < 380px)': { - gap: tokens.spacing['5'], - }, }) export const docsHeaderLogo = style({ display: 'flex', alignItems: 'center', + gap: tokens.spacing['2_5'], height: '40px', + paddingInlineStart: tokens.spacing['1'], color: 'inherit !custom', + textDecoration: 'none', +}) + +export const docsHeaderDocsLabel = style({ + paddingInlineStart: tokens.spacing['2_5'], + borderInlineStartWidth: tokens.borderWidth.hairline, + borderInlineStartStyle: 'solid', + borderInlineStartColor: inherited.color.colorMixInSrgbCurrentColor16Transparent, + color: tokens.color.foreground, + fontSize: tokens.fontSize.sm, + lineHeight: tokens.lineHeight.none, +}) + +/** The chevron beside the logo that opens the Tempo product switcher. */ +export const docsHeaderSwitcher = style({ + display: 'none', + '@media (width >= 1080px)': { + display: 'block', + }, + selectors: { + '& .docs-area-link': { + paddingInline: tokens.spacing['1_5'], + }, + }, +}) + +export const docsAreaNav = style({ + display: 'none', + '@media (width >= 1080px)': { + display: 'block', + }, + selectors: { + '& ul': { + display: 'flex', + alignItems: 'center', + gap: tokens.spacing['0_5'], + margin: 0, + padding: 0, + listStyle: 'none', + }, + }, +}) + +export const docsAreaLink = style({ + display: 'flex', + alignItems: 'center', + gap: tokens.spacing['1'], + height: '34px', + paddingBlock: tokens.spacing['0'], + paddingInline: tokens.spacing['2_5'], + border: 0, + borderRadius: tokens.radius.lg, + '--corner-radius': tokens.radius.lg, + background: 'none', + color: inherited.color.colorMixInSrgbColorForeground65Transparent, + fontFamily: tokens.fontFamily.sansFallback, + fontSize: tokens.fontSize.sm, + textDecoration: 'none', + whiteSpace: 'nowrap', + cursor: 'pointer', + transitionProperty: 'color, background-color, border-color, opacity, transform', + transitionDuration: 'var(--tempo-exit)', + transitionTimingFunction: 'var(--tempo-ease)', + selectors: { + '&:is(:hover, [aria-expanded="true"])': { + color: tokens.color.foreground, + transitionDuration: 'var(--tempo-enter)', + }, + '&:is([aria-current="page"], [aria-current="true"])': { + color: tokens.color.foreground, + backgroundColor: tokens.color.containerStrong, + }, + '& svg': { + opacity: 0.5, + transitionProperty: 'transform, translate, scale, rotate', + transitionDuration: 'var(--tempo-exit)', + }, + '&[aria-expanded="true"] svg': { + rotate: '180deg', + }, + }, +}) + +export const docsAreaMenuPanel = style({ + position: 'absolute', + top: 'calc(100% + 12px)', + insetInlineStart: 0, + display: 'grid', + gap: tokens.spacing['0_5'], + width: '340px', + padding: tokens.spacing['2'], + borderWidth: tokens.borderWidth.hairline, + borderStyle: 'solid', + borderColor: inherited.color.colorMixInSrgbColorForeground12Transparent, + borderRadius: tokens.radius.xl, + '--corner-radius': tokens.radius.xl, + backgroundColor: inherited.color.colorSurfaceShell, + // design-exception: Matches the shadow of the other header panels. + boxShadow: '0 8px 24px #0000000d !custom', + zIndex: tokens.zIndex.sectionNav, + selectors: { + '&[hidden]': { + display: 'none', + }, + '&[data-align="end"]': { + insetInlineStart: 'auto', + insetInlineEnd: 0, + }, + }, +}) + +export const docsAreaMenuItem = style({ + display: 'flex', + alignItems: 'flex-start', + gap: tokens.spacing['3'], + paddingBlock: tokens.spacing['2_5'], + paddingInline: tokens.spacing['3'], + borderRadius: tokens.radius.lg, + color: tokens.color.foreground, + textDecoration: 'none', + selectors: { + '&:is(:hover, [aria-current="page"])': { + backgroundColor: inherited.color.colorSurfaceBlock, + }, + '&:focus-visible': { + // design-exception: Inset ring so the focus outline isn't clipped by the panel edge. + outline: '2px solid currentColor !custom', + outlineOffset: '-2px', + }, + '& svg': { + flexShrink: 0, + marginTop: tokens.spacing['0_5'], + }, + }, +}) + +export const docsAreaMenuItemLabel = style({ + display: 'grid', + flex: 1, + gap: tokens.spacing['0_5'], + fontSize: tokens.fontSize.sm, + lineHeight: tokens.lineHeight.snug, +}) + +export const docsAreaMenuItemDescription = style({ + color: inherited.color.colorMixInSrgbColorForeground60Transparent, + fontSize: tokens.fontSize.compact, +}) + +/** Search: an icon below Display widths, a field with its shortcut from 1280px. */ +export const docsHeaderSearchField = style({ + display: 'grid', + width: '36px', + height: '36px', + placeItems: 'center', + border: 0, + borderRadius: tokens.radius.lg, + '--corner-radius': tokens.radius.lg, + background: 'none', + color: 'inherit !custom', + fontFamily: tokens.fontFamily.sansFallback, + cursor: 'pointer', + selectors: { + '&:hover': { + backgroundColor: inherited.color.colorMixInSrgbCurrentColor5Transparent, + }, + }, + '@media (width >= 1280px)': { + display: 'flex', + justifyContent: 'flex-start', + gap: tokens.spacing['2'], + width: '208px', + height: '34px', + paddingInline: tokens.spacing['3'], + backgroundColor: inherited.color.colorSurfaceShell, + color: inherited.color.colorMixInSrgbColorForeground65Transparent, + fontSize: tokens.fontSize.sm, + selectors: { + '&:hover': { + backgroundColor: inherited.color.colorSurfaceShell, + color: tokens.color.foreground, + }, + }, + }, +}) + +export const docsHeaderSearchLabel = style({ + display: 'none', + '@media (width >= 1280px)': { + display: 'inline', + flex: 1, + textAlign: 'start', + }, +}) + +export const docsHeaderKbd = style({ + display: 'none', + '@media (width >= 1280px)': { + display: 'inline-flex', + paddingInline: tokens.spacing['1_5'], + borderRadius: tokens.radius.sm, + backgroundColor: tokens.color.container, + color: inherited.color.colorMixInSrgbColorForeground65Transparent, + fontFamily: tokens.fontFamily.code, + fontSize: tokens.fontSize.caption, + lineHeight: tokens.lineHeight.caption, + }, +}) + +export const docsHeaderLoginText = style({ + display: 'none', + alignItems: 'center', + gap: tokens.spacing['1_5'], + height: '34px', + paddingInline: tokens.spacing['2_5'], + borderRadius: tokens.radius.lg, + '--corner-radius': tokens.radius.lg, + color: tokens.color.foreground, + fontSize: tokens.fontSize.sm, + textDecoration: 'none', + whiteSpace: 'nowrap', + selectors: { + '&:hover': { + backgroundColor: tokens.color.containerStrong, + }, + }, + '@media (width >= 1080px)': { + display: 'flex', + }, +}) + +/** Get API key: the one filled button in the global nav. */ +export const docsHeaderGetKey = style({ + display: 'none', + alignItems: 'center', + height: '34px', + paddingInline: tokens.spacing['3_5'], + borderRadius: tokens.radius.lg, + '--corner-radius': tokens.radius.lg, + backgroundColor: inherited.color.backgroundColorInvert, + color: inherited.color.textColorInvert, + fontSize: tokens.fontSize.sm, + textDecoration: 'none', + whiteSpace: 'nowrap', + transitionProperty: 'opacity', + transitionDuration: 'var(--tempo-exit)', + transitionTimingFunction: 'var(--tempo-ease)', + '@media (width >= 768px)': { + display: 'flex', + }, + '@media (hover: hover)': { + selectors: { + '&:hover': { + opacity: 0.9, + transitionDuration: 'var(--tempo-enter)', + }, + }, + }, }) export const docsHeaderMobileSearch = style({ @@ -145,7 +419,7 @@ export const docsHeaderAgentTrigger = style({ paddingInlineStart: tokens.spacing['4'], paddingInlineEnd: tokens.spacing['3'], color: inherited.color.vocsTextColorPrimary, - backgroundColor: tokens.color.container, + background: 'none', fontSize: tokens.fontSize.sm, whiteSpace: 'nowrap', cursor: 'pointer', @@ -440,20 +714,6 @@ export const docsHeaderMobileAgents = style({ }, }) -export const docsHeaderMobileUtilityLink = style({ - display: 'flex', - justifyContent: 'space-between', - paddingBlock: tokens.spacing['2'], - color: tokens.color.foreground, - fontSize: tokens.fontSize.sm, - textDecoration: 'none', - selectors: { - '& span': { - opacity: 0.4, - }, - }, -}) - export const docsHeaderMobileWebsite = style({ display: 'flex', alignItems: 'center', @@ -528,115 +788,10 @@ export const docsSectionIcon = style({ flexShrink: 0, }) -export const docsSectionUtilities = style({ - display: 'flex', - alignItems: 'center', - flexShrink: 0, - gap: tokens.spacing['6'], - marginInlineStart: tokens.spacing['3'], - marginInlineEnd: tokens.spacing['5'], - '@media (width < 800px)': { - display: 'none', - }, - '@media (width >= 1080px)': { - marginInlineEnd: tokens.spacing['7'], - }, -}) - export const docsReferenceMenu = style({ position: 'relative', }) -export const docsReferenceTrigger = style({ - position: 'relative', - display: 'flex', - alignItems: 'center', - gap: tokens.spacing['1_5'], - height: 'var(--tempo-docs-section-nav-height)', - - paddingBlock: tokens.spacing['0'], - paddingInline: tokens.spacing['0_5'], - - color: inherited.color.colorMixInSrgbColorForeground65Transparent, - fontFamily: tokens.fontFamily.sansFallback, - fontSize: tokens.fontSize.compact, - textDecoration: 'none', - whiteSpace: 'nowrap', - cursor: 'pointer', - selectors: { - '&:is(:hover, [aria-expanded="true"], [aria-current="page"])': { - color: tokens.color.foreground, - }, - '&[aria-current="page"]::after': { - content: '""', - position: 'absolute', - inset: 'auto 0 0', - height: '2px', - // design-exception: Preserve this surface's layered artwork or focus treatment. - background: 'currentColor !custom', - }, - }, - background: 'none', - border: 0, -}) - -export const docsSectionUtilityLink = style({ - position: 'relative', - display: 'flex', - alignItems: 'center', - gap: tokens.spacing['1_5'], - height: 'var(--tempo-docs-section-nav-height)', - - paddingBlock: tokens.spacing['0'], - paddingInline: tokens.spacing['0_5'], - - color: inherited.color.colorMixInSrgbColorForeground65Transparent, - fontFamily: tokens.fontFamily.sansFallback, - fontSize: tokens.fontSize.compact, - textDecoration: 'none', - whiteSpace: 'nowrap', - cursor: 'pointer', - selectors: { - '&:is(:hover, [aria-current="page"])': { - color: tokens.color.foreground, - }, - '&[aria-current="page"]::after': { - content: '""', - position: 'absolute', - inset: 'auto 0 0', - height: '2px', - // design-exception: Preserve this surface's layered artwork or focus treatment. - background: 'currentColor !custom', - }, - }, -}) - -export const docsReferencePanel = style({ - position: 'absolute', - top: 'calc(100% + 8px)', - insetInlineEnd: 0, - width: '280px', - padding: tokens.spacing['3'], - borderWidth: tokens.borderWidth.hairline, - borderStyle: 'solid', - borderColor: inherited.color.colorMixInSrgbColorForeground12Transparent, - borderRadius: tokens.radius.lg, - backgroundColor: inherited.color.colorSurfaceShell, - // design-exception: Preserve this surface's layered artwork or focus treatment. - boxShadow: '0 8px 24px #0000000d !custom', - selectors: { - '&[hidden]': { - display: 'none', - }, - '& a:focus-visible': { - // design-exception: Preserve this surface's layered artwork or focus treatment. - outline: '2px solid currentColor !custom', - outlineOffset: '-2px', - }, - }, - zIndex: tokens.zIndex.sectionNav, -}) - export const docsResourceLinks = style({ display: 'grid', gridTemplateColumns: '1fr', @@ -702,71 +857,6 @@ export const docsHeaderMobileTheme = style({ }, }) -export const docsApiMenuMobile = style({ - selectors: { - '& .docs-reference-trigger': { - justifyContent: 'space-between', - width: '100%', - height: 'auto', - - paddingBlock: tokens.spacing['2'], - paddingInline: tokens.spacing['0'], - color: tokens.color.foreground, - fontSize: tokens.fontSize.sm, - }, - '& .docs-reference-trigger[aria-current="page"]::after': { - display: 'none', - }, - '& .docs-reference-trigger svg': { - opacity: 0.4, - transition: 'rotate 150ms', - }, - '& .docs-reference-trigger[aria-expanded="true"] svg': { - rotate: '180deg', - }, - '& .docs-reference-panel': { - position: 'static', - width: '100%', - padding: tokens.spacing['0'], - border: 0, - backgroundColor: 'transparent !custom', - boxShadow: 'none', - gap: tokens.spacing['0'], - marginBottom: tokens.spacing['2'], - }, - '& .docs-reference-panel a': { - paddingBlock: tokens.spacing['2'], - paddingInlineStart: tokens.spacing['4'], - paddingInlineEnd: tokens.spacing['0'], - }, - '& .docs-reference-panel a:hover': { - backgroundColor: 'transparent !custom', - }, - '& .docs-api-menu-item > svg:last-child:not(:first-child)': { - marginInlineStart: 'auto !custom', - opacity: 0.4, - }, - }, - '@media (prefers-reduced-motion: reduce)': { - selectors: { - '& .docs-reference-trigger svg': { - transition: 'none', - }, - }, - }, -}) - -export const docsApiMenuItem = style({ - display: 'flex', - alignItems: 'center', - gap: tokens.spacing['3'], - selectors: { - '& svg': { - flexShrink: 0, - }, - }, -}) - /** Signed-in email in the header's account link, cut short (and hidden on narrow screens) so the logo keeps its width. */ export const docsHeaderAccountEmail = style({ maxWidth: '24ch', @@ -779,16 +869,60 @@ export const docsHeaderAccountEmail = style({ }, }) -/** Signed-out Log in link: the header's one primary action, inverted like Copy for agent. */ -export const docsHeaderLoginPrimary = style({ - backgroundColor: inherited.color.backgroundColorInvert, - color: inherited.color.textColorInvert, - '@media (hover: hover)': { - selectors: { - '&:hover': { - backgroundColor: inherited.color.backgroundColorInvert, - opacity: 0.9, - }, +/** The current area's one-line description at the end of the section row. */ +export const docsSectionNavDescription = style({ + display: 'none', + flexShrink: 0, + marginBlock: tokens.spacing['0'], + marginInlineEnd: tokens.spacing['7'], + color: inherited.color.colorMixInSrgbColorForeground60Transparent, + fontSize: tokens.fontSize.compact, + whiteSpace: 'nowrap', + '@media (width >= 1080px)': { + display: 'block', + }, +}) + +export const docsHeaderMobileAreaLabel = style({ + marginBlock: tokens.spacing['0'], + paddingTop: tokens.spacing['3'], + paddingBottom: tokens.spacing['1'], + color: inherited.color.colorMixInSrgbColorForeground55Transparent, + fontSize: tokens.fontSize.xs, +}) + +export const docsHeaderMobileAreaGroup = style({ + display: 'grid', + selectors: { + '& a': { + paddingInlineStart: tokens.spacing['3'], + }, + }, +}) + +/** Log in and Get API key at the foot of the mobile menu. */ +export const docsHeaderMobileAccount = style({ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: tokens.spacing['2'], + marginTop: tokens.spacing['6'], + selectors: { + '& a': { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '44px', + borderRadius: tokens.radius.lg, + fontSize: tokens.fontSize.sm, + textDecoration: 'none', + }, + '& a:first-child': { + backgroundColor: tokens.color.container, + color: tokens.color.foreground, + }, + '& a:last-child': { + backgroundColor: inherited.color.backgroundColorInvert, + color: inherited.color.textColorInvert, }, }, }) diff --git a/src/components/DocsSectionNav.tsx b/src/components/DocsSectionNav.tsx index 7f4a87ec8..8c30019b7 100644 --- a/src/components/DocsSectionNav.tsx +++ b/src/components/DocsSectionNav.tsx @@ -3,41 +3,47 @@ import { useEffect, useId, useRef, useState } from 'react' import { Link, useRouter } from 'waku' import ArrowUpRightIcon from '~icons/lucide/arrow-up-right' +import BookOpenIcon from '~icons/lucide/book-open' import BracesIcon from '~icons/lucide/braces' import ChevronDownIcon from '~icons/lucide/chevron-down' -import CodeIcon from '~icons/lucide/code-xml' import CompassIcon from '~icons/lucide/compass' -import ScanSearchIcon from '~icons/lucide/scan-search' +import HandshakeIcon from '~icons/lucide/handshake' +import ServerIcon from '~icons/lucide/server' import TerminalIcon from '~icons/lucide/terminal' import { + type DocsArea, type DocsSection, + docsAreas, docsSections, docsUtilitySections, getActiveDocsSection, + getDocsAreaForSection, + getDocsSectionById, } from '../lib/docs-sections' import { docsSectionNav as sectionSurface } from '../styles/surfaces.styles' import { docsProductIcons } from './DocsHomeProductIcon' import { - docsApiMenuItem, - docsApiMenuMobile, + docsAreaLink, + docsAreaMenuItem, + docsAreaMenuItemDescription, + docsAreaMenuItemLabel, + docsAreaMenuPanel, + docsAreaNav, docsReferenceMenu, - docsReferencePanel, - docsReferenceTrigger, docsResourceLinkLabel, docsResourceLinks, docsResourceLinkText, docsResourceLogo, docsSectionIcon, docsSectionNav, + docsSectionNavDescription, docsSectionNavScroll, - docsSectionUtilities, - docsSectionUtilityLink, } from './DocsNavigation.styles' import { MercatorLogo, MppLogo, TempoMark } from './ToolLogos' export { docsSections, docsUtilitySections, getActiveDocsSection } -const sectionIcons: Partial> = { +export const sectionIcons: Partial> = { overview: CompassIcon, accounts: docsProductIcons.accounts, earn: docsProductIcons.earn, @@ -45,6 +51,24 @@ const sectionIcons: Partial> = { zones: docsProductIcons.zones, 'machine-payments': docsProductIcons.agents, developers: docsProductIcons.network, + operate: ServerIcon, + api: BracesIcon, + tools: TerminalIcon, + resources: BookOpenIcon, + ecosystem: HandshakeIcon, +} + +/** One line per section in the area menus. */ +export const sectionDescriptions: Partial> = { + accounts: 'Stablecoin accounts with security rules built in', + earn: 'Earning balances for stablecoins', + routes: 'Deposits and transfers across networks', + zones: 'Private zones on Tempo', + 'machine-payments': 'Charge for APIs and pay for services', + api: 'Endpoints, authentication and conventions', + tools: 'TypeScript, Rust, Go, Python, Foundry and the CLI', + resources: 'Plan payment workflows and integrations', + ecosystem: 'Wallets, ramps, bridges and infrastructure', } export const docsExternalTools = [ @@ -54,6 +78,12 @@ export const docsExternalTools = [ { label: 'Tempo Wallet', href: 'https://wallet.tempo.xyz/', logo: TempoMark }, ] +export function areaSections(area: DocsArea) { + return area.sections + .map((id) => getDocsSectionById(id)) + .filter((section): section is DocsSection => Boolean(section)) +} + export function DocsResourceLinks({ onNavigate }: { onNavigate?: () => void }) { return (
) } +function AreaMenuItem({ + section, + active, + onNavigate, +}: { + section: DocsSection + active: boolean + onNavigate: () => void +}) { + const Icon = sectionIcons[section.id] + return ( + + {Icon && ( +