diff --git a/AGENTS.md b/AGENTS.md
index 7a1e3066f..995a78e3d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -17,10 +17,12 @@ 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, and wallet CLI pages (`/docs/sdk`, `/docs/cli`, `/docs/wallet`) 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 `docsResourceSectionIds`, the sections in the top bar's Resources menu; `vocs.config.ts` owns the sidebar within each section.
+- Top bar: the TEMPO DEVELOPERS lockup (Home), the products as plain links (Get started, Accounts, Earn, Routes, Zones, Machine Payments, Network), then Resources, the only menu (Guides, Partners, SDKs & CLI), search and Get API key, the one primary button. There is no second row. The sidebar lists only the current section's pages, and each product's sidebar ends with an API reference group that links its endpoint pages; the API reference itself keeps the full sidebar generated from the spec. Below 1200px the products move into the menu drawer (products, Resources, then the section's pages); the Vocs sidebar shows from 1080px. Each docs page opens with a header row: Section › Page breadcrumbs, Ask AI and Copy page. Docs pages end at Previous / Next with no footer; Home and Not found keep the full footer.
+- Sections: Get started (`/get-started`, onboarding plus the testnet faucet, network details, and Build with AI), Accounts (`/docs/accounts`), Earn (`/docs/earn`), Routes (`/docs/routes`), Zones (`/docs/zones`), Machine Payments (`/docs/agents`), Network (`/docs/development`, plus `/docs/guide/node`, `/docs/changelog`, and `/docs/protocol/upgrades`), API (`/docs/api`), SDKs & CLI (`/docs/sdk`, `/docs/cli`, `/docs/wallet`), Guides (`/docs/resources`) and Partners (`/docs/partners`).
+- Network merges the former Tempo EVM and Operate sections under the `developers` id. Its sidebar lists the Tempo EVM groups (wallets and signing keys, transactions, fees, TIP-20 Tokens, contracts, exchange, network details, and protocol specifications), then Run a node, then Changelog (network upgrades, node releases, and operator updates).
+- The header has no product switcher, agent popover, or sign-in link. Their links live in context: Tempo Wallet in the Accounts sidebar, the MPP spec in Machine Payments, the Explorer in Network, agent setup in Get started › Build with AI, and Console behind Get API key (it reads "Console" when signed in).
+- 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/e2e/blog.test.ts b/e2e/blog.test.ts
index e3b268e39..ef80a4984 100644
--- a/e2e/blog.test.ts
+++ b/e2e/blog.test.ts
@@ -51,24 +51,34 @@ for (const width of [390, 1440]) {
await expect(
page.locator('header').getByRole('link', { name: 'Blog', exact: true, includeHidden: true }),
).toHaveCount(0)
- await expect(
- page
- .locator('.docs-section-utilities')
- .getByRole('link', { name: 'Blog', exact: true, includeHidden: true }),
- ).toHaveCount(0)
await expect(page.locator('#blog, .tempo-blog')).toHaveCount(0)
- if (width < 1080) {
+ if (width < 1200) {
await page.getByRole('button', { name: 'Open menu', exact: true }).click()
- const menu = page.getByRole('dialog', { name: 'Documentation navigation', exact: true })
+ const menu = page.getByRole('dialog', { name: 'Tempo Developers menu', exact: true })
await expect(menu).toBeVisible()
await expect(
menu.getByRole('link', { name: 'Blog', exact: true, includeHidden: true }),
).toHaveCount(0)
- await expect(menu.getByRole('link', { name: 'Get Started', exact: true })).toBeVisible()
+ await expect(menu.getByRole('link', { name: 'Get started', exact: true })).toBeVisible()
} else {
+ const products = page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
+ await expect(products).toBeVisible()
+ await expect(products.getByRole('link')).toHaveText([
+ 'Get started',
+ 'Accounts',
+ 'Earn',
+ 'Routes',
+ 'Zones',
+ 'Machine Payments',
+ 'Network',
+ ])
+ // Resources, the bar's only menu, opens outside the header.
+ await products.getByRole('button', { name: 'Resources', exact: true }).click()
await expect(
- page.getByRole('navigation', { name: 'Documentation sections', exact: true }),
- ).toBeVisible()
+ page.getByRole('menu', { name: 'Resources', exact: true }).getByRole('menuitem'),
+ ).toHaveText(['Guides', 'Partners', 'SDKs & CLI'])
}
})
}
diff --git a/e2e/developers-homepage.test.ts b/e2e/developers-homepage.test.ts
index 94282b058..0951b9ede 100644
--- a/e2e/developers-homepage.test.ts
+++ b/e2e/developers-homepage.test.ts
@@ -16,8 +16,14 @@ test('serves the docs homepage directly at the root URL', async ({ page, request
expect((await request.get('/', { maxRedirects: 0 })).status()).toBe(200)
await page.goto('/')
await expect(page).toHaveURL((url) => url.pathname === '/')
- await expect(page.getByRole('heading', { level: 1, name: 'Documentation' })).toBeVisible()
- await expect(page.getByRole('navigation', { name: 'Developer navigation' })).toBeVisible()
+ await expect(
+ page.getByRole('heading', { level: 1, name: 'Build stablecoin payments on Tempo' }),
+ ).toBeVisible()
+ await expect(
+ page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true }),
+ ).toBeVisible()
})
test('keeps legacy docs queries and anchors when opening the new landing', async ({ page }) => {
@@ -40,7 +46,7 @@ test('serves getting started directly with its overview sidebar', async ({ page,
).toBeVisible()
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
await expect(sidebar).toBeVisible()
- await expect(sidebar.getByRole('link', { name: 'Start building', exact: true })).toHaveAttribute(
+ await expect(sidebar.getByRole('link', { name: 'Build on Tempo', exact: true })).toHaveAttribute(
'href',
'/get-started',
)
@@ -84,24 +90,30 @@ for (const entry of ['direct', 'client navigation'] as const) {
} else {
await page.goto('/docs/accounts')
await page
- .getByRole('navigation', { name: 'Documentation sections' })
- .getByRole('link', { name: 'Get Started', exact: true })
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
+ .getByRole('link', { name: 'Get started', exact: true })
.click()
await page
.locator('article[data-v-content]')
- .getByRole('link', { name: 'interactive quickstart', exact: true })
+ .getByRole('link', { name: 'Send your first payment', exact: true })
.click()
}
await expect(page).toHaveURL((url) => url.pathname === '/get-started/quickstart')
await expect(page.locator('#try-a-payment')).toBeAttached()
await expect(page.locator('h2#try-a-payment')).toBeVisible()
- const pageTools = page.getByRole('navigation', { name: 'Page tools' })
- await expect(pageTools).toHaveCount(1)
- await expect(pageTools.getByRole('link', { name: 'View Markdown' })).toHaveAttribute(
- 'href',
- '/assets/md/get-started/quickstart.md',
- )
+ // One Copy page split button above the H1; its menu links the page's Markdown.
+ const pageActions = page.locator('.docs-page-actions')
+ await expect(pageActions).toHaveCount(1)
+ await expect(pageActions.getByRole('button', { name: 'Copy page', exact: true })).toBeVisible()
+ await pageActions.getByRole('button', { name: 'More page options', exact: true }).click()
+ const pageMenu = page.getByRole('menu', { name: 'More page options', exact: true })
+ await expect(
+ pageMenu.getByRole('menuitem', { name: 'View as Markdown', exact: true }),
+ ).toHaveAttribute('href', '/assets/md/get-started/quickstart.md')
+ await page.keyboard.press('Escape')
+ await expect(pageMenu).toHaveCount(0)
const signIn = getDemoStep(page, 'Create an account, or use an existing one.').getByRole(
'button',
{ name: 'Sign in', exact: true },
@@ -163,14 +175,18 @@ for (const entry of ['direct', 'client navigation'] as const) {
}) => {
await page.setViewportSize({ width: 1440, height: 1000 })
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
- const sections = page.getByRole('navigation', { name: 'Documentation sections' })
+ const products = page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
if (entry === 'direct') {
expect((await request.get('/get-started/quickstart', { maxRedirects: 0 })).status()).toBe(200)
await page.goto('/get-started/quickstart')
} else {
await page.goto('/docs/accounts')
- await sections.getByRole('link', { name: 'Get Started', exact: true }).click()
+ // Get started opens its overview.
+ await products.getByRole('link', { name: 'Get started', exact: true }).click()
+ await expect(page).toHaveURL(/\/get-started\/?$/)
await sidebar.getByRole('link', { name: 'Quickstart', exact: true }).click()
}
@@ -178,18 +194,21 @@ for (const entry of ['direct', 'client navigation'] as const) {
await expect(
page.getByRole('heading', { level: 1, name: 'Quickstart', exact: true }),
).toBeVisible()
- await expect(page.getByRole('navigation', { name: 'Developer navigation' })).toBeVisible()
- await expect(sections).toBeVisible()
- await expect(sections.locator('[aria-current="page"]')).toHaveText('Get Started')
+ await expect(products).toBeVisible()
+ await expect(products.locator('[aria-current="page"]')).toHaveText('Get started')
+ await expect(products.getByRole('link', { name: 'Get started', exact: true })).toHaveAttribute(
+ 'aria-current',
+ 'page',
+ )
await expect(sidebar).toBeVisible()
await expect(sidebar.locator('a[data-active]')).toHaveAttribute(
'href',
'/get-started/quickstart',
)
- await sidebar.getByRole('link', { name: 'Start building', exact: true }).click()
+ await sidebar.getByRole('link', { name: 'Build on Tempo', exact: true }).click()
await expect(page).toHaveURL(/\/get-started\/?$/)
- await expect(sections).toBeVisible()
+ await expect(products.locator('[aria-current="page"]')).toHaveText('Get started')
await expect(sidebar.locator('a[data-active]')).toHaveAttribute('href', '/get-started')
})
}
diff --git a/e2e/docs-workspace.test.ts b/e2e/docs-workspace.test.ts
index 8ebeeb07f..b210ea0ca 100644
--- a/e2e/docs-workspace.test.ts
+++ b/e2e/docs-workspace.test.ts
@@ -1,67 +1,139 @@
-import { expect, type Locator, test } from '@playwright/test'
+import { expect, type Locator, type Page, test } from '@playwright/test'
-const sectionLabels = [
- 'Get Started',
+// From 1200px the top bar lists the products as plain links, then Resources, its only menu.
+const productLabels = [
+ 'Get started',
'Accounts',
'Earn',
'Routes',
'Zones',
'Machine Payments',
- 'Tempo EVM',
+ 'Network',
]
-const utilityLabels = ['API reference', 'Partners']
-const externalTools = [
- ['MPP', 'https://mpp.dev/'],
- ['Mercator', 'https://mercator.sh/'],
- ['Tempo Console', 'https://console.tempo.xyz/'],
- ['Tempo Wallet', 'https://wallet.tempo.xyz/'],
+const resourceSections = [
+ ['Guides', '/docs/resources/guides'],
+ ['Partners', '/docs/partners'],
+ ['SDKs & CLI', '/docs/sdk'],
] as const
-
-const homeAgentSetups = [
- {
- label: 'Codex',
- command:
- 'codex plugin marketplace add tempoxyz/plugins --ref main\ncodex plugin add docs@tempo',
- instruction: null,
- install: /^Install Codex CLI/,
- },
- {
- label: 'Claude Code',
- command: 'claude plugin marketplace add tempoxyz/plugins\nclaude plugin install docs@tempo',
- instruction: null,
- install: /^Install Claude Code/,
- },
- {
- label: 'Amp',
- command: 'amp mcp add tempo https://mcp.tempo.xyz',
- instruction: /MCP/,
- install: /^Install Amp/,
- },
- {
- label: 'Skills',
- command: 'npx skills add tempoxyz/plugins --skill docs',
- instruction: /skill/i,
- install: null,
- },
- {
- label: 'MCP',
- command: 'https://mcp.tempo.xyz',
- instruction: /HTTP MCP server/i,
- install: null,
- },
+const resourceLabels: string[] = resourceSections.map(([label]) => label)
+// Below 1200px the menu drawer lists the products, then Resources' sections under its label.
+const menuLinks = [...productLabels, ...resourceLabels]
+// The other Tempo products are linked from the full footer on Home; docs pages have no footer.
+const footerTools = [
+ ['tempo.xyz', 'https://tempo.xyz'],
+ ['Console', 'https://console.tempo.xyz'],
+ ['Wallet', 'https://wallet.tempo.xyz'],
+ ['Explorer', 'https://explore.tempo.xyz'],
+ ['MPP', 'https://mpp.dev'],
+ ['Mercator', 'https://mercator.sh'],
+ ['GitHub', 'https://github.com/tempoxyz'],
+ ['X', 'https://twitter.com/tempo'],
] as const
-test('seven sections expose product tasks and expandable developer chapters', async ({ page }) => {
+// Vocs marks the desktop sidebar's current page with data-active; the drawer's DS items use
+// aria-current and set data-active="false" on every other page.
+const activeSidebarLink = 'a[data-active="true"], a[aria-current="page"]'
+
+/** The products in the top bar (1200px and up). */
+function productNav(page: Page) {
+ return page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
+}
+
+/** The Resources menu button after the products. */
+function resourcesButton(page: Page) {
+ return productNav(page).getByRole('button', { name: 'Resources', exact: true })
+}
+
+/** The Resources menu, named after its button. */
+function resourcesMenu(page: Page) {
+ return page.getByRole('menu', { name: 'Resources', exact: true })
+}
+
+/** Below 1200px, Open menu opens the menu drawer. */
+function menuButton(page: Page) {
+ return page.getByRole('button', { name: 'Open menu', exact: true })
+}
+
+/** The menu drawer: the products and Resources, then the current section's pages. */
+function menuDrawer(page: Page) {
+ return page.getByRole('dialog', { name: 'Tempo Developers menu', exact: true })
+}
+
+/** The current section's pages in the menu drawer, named after the section. */
+function sectionPages(page: Page, section: string) {
+ return menuDrawer(page).getByRole('navigation', { name: `${section} pages`, exact: true })
+}
+
+/** The breadcrumb above a docs page's H1: the section, then the current page. */
+function breadcrumbItems(page: Page) {
+ return page
+ .locator('.docs-page-header')
+ .getByRole('navigation', { name: 'Breadcrumb', exact: true })
+ .getByRole('listitem')
+}
+
+/** The Copy page split button's menu, named after its More page options button. */
+function pageOptionsMenu(page: Page) {
+ return page.getByRole('menu', { name: 'More page options', exact: true })
+}
+
+/**
+ * The current section: its product in the top bar from 1200px, the breadcrumb's section below.
+ * Guides, Partners and SDKs & CLI mark the Resources button instead; the API reference has no
+ * top-bar entry, so only the breadcrumb names it.
+ */
+async function expectSection(page: Page, section: string, mobile = false) {
+ if (mobile) {
+ await expect(breadcrumbItems(page).first()).toHaveText(section)
+ return
+ }
+ const current = productNav(page).locator('[aria-current="page"]')
+ if (productLabels.includes(section)) {
+ await expect(current).toBeVisible()
+ await expect(current).toHaveText(section)
+ return
+ }
+ await expect(current).toHaveCount(0)
+ await expect(breadcrumbItems(page).first()).toHaveText(section)
+ if (resourceLabels.includes(section))
+ await expect(resourcesButton(page)).toHaveAttribute('data-active', '')
+ else await expect(resourcesButton(page)).not.toHaveAttribute('data-active')
+}
+
+test('the top bar lists the products and Resources, and products expose their tasks and developer chapters', async ({
+ page,
+}) => {
await page.setViewportSize({ width: 1440, height: 1000 })
await page.goto('/')
- const navigation = page.getByRole('navigation', { name: 'Documentation sections', exact: true })
- const utilities = page.locator('.docs-section-utilities')
+ const products = productNav(page)
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
- await expect(navigation.getByRole('link')).toHaveText(sectionLabels)
- await expect(utilities.getByRole('link')).toHaveText(utilityLabels)
+ await expect(products.getByRole('link')).toHaveText(productLabels)
+ // Products are plain links; Resources is the only menu.
+ await expect(products.getByRole('button')).toHaveText(['Resources'])
+ // Home belongs to no section.
+ await expect(products.locator('[aria-current]')).toHaveCount(0)
+ await expect(resourcesButton(page)).not.toHaveAttribute('data-active')
+ // Clicks before hydration do nothing; the header search enables once hydrated.
+ await expect(
+ page.getByRole('button', { name: 'Search documentation', exact: true }),
+ ).toBeEnabled()
+ for (const [label, href] of resourceSections) {
+ await resourcesButton(page).click()
+ await expect(resourcesMenu(page).getByRole('menuitem')).toHaveText(resourceLabels)
+ await resourcesMenu(page).getByRole('menuitem', { name: label, exact: true }).click()
+ await expect(page).toHaveURL(new RegExp(`${href}/?$`))
+ await expect(resourcesMenu(page)).toBeHidden()
+ await expectSection(page, label)
+ }
+ await resourcesButton(page).click()
+ await expect(resourcesMenu(page).locator('[aria-current="page"]')).toHaveText('SDKs & CLI')
+ await page.keyboard.press('Escape')
+ await expect(resourcesMenu(page)).toBeHidden()
for (const section of [
{
- label: 'Get Started',
+ label: 'Get started',
path: '/get-started',
links: ['/get-started/quickstart', '/docs/guide/using-tempo-with-ai'],
},
@@ -129,17 +201,21 @@ test('seven sections expose product tasks and expandable developer chapters', as
],
},
{
- label: 'Tempo EVM',
+ // Network merges Tempo EVM and Operate: building on the chain, nodes, upgrades and releases.
+ label: 'Network',
path: '/docs/development',
- links: ['/docs/development'],
+ links: [
+ '/docs/development',
+ '/docs/guide/node',
+ '/docs/protocol/upgrades',
+ '/docs/changelog',
+ ],
},
]) {
- await navigation.getByRole('link', { name: section.label, exact: true }).click()
+ await products.getByRole('link', { name: section.label, exact: true }).click()
await expect(page).toHaveURL(new RegExp(`${section.path}/?$`))
- await expect(navigation.locator('[aria-current="page"]')).toHaveText(
- section.label.replace(/ \(.*\)$/, ''),
- )
- await expect(utilities.locator('[aria-current]')).toHaveCount(0)
+ // Only the section's own product is marked current in the top bar.
+ await expect(products.locator('[aria-current]')).toHaveText([section.label])
await expect(page.locator('#related-documentation')).toHaveCount(0)
for (const href of section.links)
await expect(sidebar.locator(`a[href="${href}"]`), `${section.label}: ${href}`).toBeVisible()
@@ -150,18 +226,19 @@ test('seven sections expose product tasks and expandable developer chapters', as
await expect(specifications).toBeVisible()
await specifications.click()
await expect(page).toHaveURL(/\/docs\/protocol\/?$/)
- await expect(navigation.locator('[aria-current="page"]')).toHaveText('Tempo EVM')
+ await expectSection(page, 'Network')
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
})
for (const section of [
{
- label: 'Get Started',
+ label: 'Get started',
overview: '/get-started',
paths: [
'/get-started/quickstart',
'/docs/guide/using-tempo-with-ai',
'/docs/quickstart/faucet',
+ '/docs/quickstart/connection-details',
],
},
{
@@ -196,7 +273,7 @@ for (const section of [
],
},
{
- label: 'Tempo EVM',
+ label: 'Network',
overview: '/docs/development',
paths: [
'/docs/network/transactions',
@@ -205,13 +282,11 @@ for (const section of [
'/docs/guide/payments/transfer-memos',
'/docs/guide/payments/send-a-payment/examples',
'/docs/guide/getting-funds',
- '/docs/quickstart/connection-details',
'/docs/quickstart/wallet-developers',
'/docs/protocol/rpc',
'/docs/guide/issuance/create-a-stablecoin',
'/docs/guide/stablecoin-dex/executing-swaps',
'/docs/guide/issuance/manage-stablecoin',
- '/docs/guide/node/installation',
],
},
{
@@ -226,8 +301,8 @@ for (const section of [
],
},
{
- label: 'Get Started (SDKs and CLI)',
- overview: '/get-started',
+ label: 'SDKs & CLI',
+ overview: '/docs/sdk',
paths: ['/docs/sdk', '/docs/cli', '/docs/cli/api', '/docs/sdk/typescript/api-client'],
},
{
@@ -240,19 +315,24 @@ for (const section of [
],
},
{
- label: 'API reference',
+ label: 'API',
overview: '/docs/api/console',
paths: ['/docs/api/console', '/docs/api/console/api-keys', '/docs/api/team'],
},
{
- label: 'Tempo EVM (Changelog)',
+ label: 'Network (node operators)',
+ overview: '/docs/guide/node',
+ paths: ['/docs/guide/node/installation'],
+ },
+ {
+ label: 'Network (Changelog)',
overview: '/docs/protocol/upgrades',
paths: ['/docs/protocol/upgrades/t11', '/docs/changelog'],
},
]) {
test(`existing deep links select ${section.label} and retain their sidebar`, async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 })
- const navigation = page.locator('.docs-section-nav')
+ const label = section.label.replace(/ \(.*\)$/, '')
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
for (const path of section.paths) {
const [pathname, anchor] = path.split('#')
@@ -262,15 +342,11 @@ for (const section of [
)
if (anchor) await expect(page.locator(`[id="${anchor}"]`)).toHaveCount(1)
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
- await expect(navigation.locator('[aria-current="page"]')).toHaveText(
- section.label.replace(/ \(.*\)$/, ''),
- )
+ await expectSection(page, label)
await expect(sidebar.locator(`a[href="${section.overview}"]`).first()).toBeVisible()
}
expect((await page.reload())?.status()).toBe(200)
- await expect(navigation.locator('[aria-current="page"]')).toHaveText(
- section.label.replace(/ \(.*\)$/, ''),
- )
+ await expectSection(page, label)
})
}
@@ -280,9 +356,9 @@ test('Get started recommends a test payment and offers build paths and tool link
await page.goto('/get-started')
const article = page.locator('article[data-v-content]')
await expect(article.getByRole('heading', { level: 1 })).toHaveText('Build on Tempo')
- const quickstart = article.getByRole('link', { name: 'interactive quickstart', exact: true })
+ const quickstart = article.getByRole('link', { name: 'Send your first payment', exact: true })
await expect(quickstart).toHaveCount(1)
- await expect(quickstart).toHaveAccessibleName('interactive quickstart')
+ await expect(quickstart).toHaveAttribute('href', '/get-started/quickstart')
await expect(quickstart).toBeVisible()
for (const [label, href] of [
['Embed stablecoins in your app', '/docs/accounts'],
@@ -326,15 +402,11 @@ test('product availability lives in overview callouts, not navigation labels', a
await expect(article.getByRole('heading', { level: 1 })).not.toContainText(/beta|preview/i)
await expect(article).toContainText(status)
await expect(article.locator('a[href="https://tempo.xyz/contact"]').first()).toBeVisible()
- await expect(
- page
- .getByRole('navigation', { name: 'Documentation sections', exact: true })
- .getByRole('link'),
- ).toHaveText(sectionLabels)
+ await expect(productNav(page).getByRole('link')).toHaveText(productLabels)
}
})
-test('Earn task pages and API reference remain distinct', async ({ page }) => {
+test('Earn task pages and the API reference remain distinct', async ({ page }) => {
await page.goto('/docs/earn')
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
for (const [label, path] of [
@@ -347,224 +419,234 @@ test('Earn task pages and API reference remain distinct', async ({ page }) => {
await expect(page).toHaveURL(new RegExp(`${path}/?$`))
await expect(sidebar.locator('a[data-active]')).toHaveAttribute('href', path)
}
- await page
- .locator('.docs-section-utilities')
- .getByRole('link', { name: 'API reference', exact: true })
- .click()
- await expect(page).toHaveURL(/\/docs\/api\/?$/)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('API reference')
+ // Earn's sidebar ends with its API reference, which belongs to the API section, not Earn.
+ await sidebar.getByRole('link', { name: 'Earn API', exact: true }).click()
+ await expect(page).toHaveURL(/\/docs\/api\/earn\/?$/)
+ await expectSection(page, 'API')
+ // A generic Overview takes its group's name.
+ await expect(breadcrumbItems(page)).toHaveText(['API', 'Earn API'])
})
-test('Changelog lives inside Tempo EVM and retains upgrade URLs', async ({ page }) => {
+test('node guides and the Changelog live in Network and retain upgrade URLs', async ({ page }) => {
await page.goto('/docs/development')
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
- await expect(page.locator('.docs-section-utilities').getByText('Changelog')).toHaveCount(0)
- const upgrades = sidebar.locator('a[href="/docs/protocol/upgrades"]')
- // A click before hydration does nothing, so retry until the chapter opens.
- await expect(async () => {
- if (!(await upgrades.isVisible()))
- await sidebar.getByRole('button', { name: 'Changelog', exact: true }).click()
- await expect(upgrades).toBeVisible({ timeout: 1000 })
- }).toPass()
- await upgrades.click()
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('Tempo EVM')
+ const current = productNav(page).locator('[aria-current="page"]')
+ await expect(productNav(page).getByText('Changelog')).toHaveCount(0)
+ // One Network sidebar carries the chain, the node guides and the Changelog.
+ await sidebar.locator('a[href="/docs/guide/node"]').click()
+ await expect(page).toHaveURL(/\/docs\/guide\/node\/?$/)
+ await expect(current).toHaveText('Network')
+ await sidebar.locator('a[href="/docs/protocol/upgrades"]').click()
+ await expect(page).toHaveURL(/\/docs\/protocol\/upgrades\/?$/)
+ await expect(current).toHaveText('Network')
await sidebar.locator('a[href="/docs/protocol/upgrades/t11"]').click()
await expect(page.getByRole('heading', { level: 1 })).toHaveText('T11 Network Upgrade')
expect((await page.reload())?.status()).toBe(200)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('Tempo EVM')
+ await expect(current).toHaveText('Network')
await sidebar.getByRole('link', { name: 'Node releases', exact: true }).click()
await expect(page).toHaveURL(/\/docs\/changelog\/?$/)
- await sidebar.locator('a[href="/docs/development"]').click()
+ await expect(current).toHaveText('Network')
+ await productNav(page).getByRole('link', { name: 'Network', exact: true }).click()
await expect(page).toHaveURL(/\/docs\/development\/?$/)
})
-test('API reference is a keyboard-accessible utility and the old tools hub redirects', async ({
+test('Resources is a keyboard-accessible menu and the old tools hub redirects', async ({
page,
}) => {
await page.goto('/')
- const api = page
- .locator('.docs-section-utilities')
- .getByRole('link', { name: 'API reference', exact: true })
- await api.focus()
+ // Keys pressed before hydration do nothing; the header search enables once hydrated.
+ await expect(
+ page.getByRole('button', { name: 'Search documentation', exact: true }),
+ ).toBeEnabled()
+ const trigger = resourcesButton(page)
+ const items = resourcesMenu(page).getByRole('menuitem')
+ await trigger.focus()
+ await page.keyboard.press('Enter')
+ await expect(items).toHaveText(resourceLabels)
+ await expect(items.first()).toBeFocused()
+ await page.keyboard.press('End')
+ await expect(items.last()).toBeFocused()
+ await page.keyboard.press('Home')
+ await expect(items.first()).toBeFocused()
await page.keyboard.press('Enter')
- await expect(page).toHaveURL(/\/docs\/api\/?$/)
- await expect(api).toHaveAttribute('aria-current', 'page')
+ await expect(page).toHaveURL(/\/docs\/resources\/guides\/?$/)
+ await expectSection(page, 'Guides')
await expect(page.locator('#related-documentation')).toHaveCount(0)
expect((await page.goto('/docs/tools'))?.status()).toBe(200)
await expect(page).toHaveURL(/\/docs\/sdk\/?$/)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('Get Started')
+ await expectSection(page, 'SDKs & CLI')
})
for (const path of ['/', '/docs/accounts']) {
- test(`mobile navigation separates products, utilities, and external tools on ${path}`, async ({
+ test(`mobile menu lists the products and Resources, and the footer links the products on ${path}`, async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(path)
- await page.getByRole('button', { name: 'Open menu', exact: true }).click()
- const menu = page.getByRole('dialog', { name: 'Documentation navigation', exact: true })
- const sections = menu.getByRole('navigation', { name: 'All documentation sections' })
- await expect(sections.getByRole('link')).toHaveCount(sectionLabels.length)
- for (const label of sectionLabels)
- await expect(sections.getByRole('link', { name: label, exact: true })).toBeVisible()
- for (const label of utilityLabels)
- await expect(menu.getByRole('link', { name: label, exact: true })).toBeVisible()
- const tools = menu.getByRole('navigation', { name: 'External tools', exact: true })
- for (const [label, href] of externalTools) {
- const link = tools.getByRole('link', { name: label, exact: true })
- await expect(link).toHaveAttribute('href', href)
- await expect(link).toHaveAttribute('target', '_blank')
- await expect(link).toHaveAttribute('rel', /noopener/)
+ const footer = page.locator('.docs-site-footer')
+ if (path === '/') {
+ await expect(footer).toHaveAttribute('data-variant', 'full')
+ await expect(
+ footer.getByRole('navigation', { name: 'Footer', exact: true }).locator('p'),
+ ).toHaveText(['Products', 'Resources', 'Tempo'])
+ // Products and Resources repeat the top bar; the API reference, which has no top-bar
+ // entry, closes Resources.
+ await expect(footer.locator('a[href^="/"]')).toHaveText([...menuLinks, 'API reference'])
+ for (const [label, href] of footerTools) {
+ const link = footer.getByRole('link', {
+ name: `${label} (opens in a new tab)`,
+ exact: true,
+ })
+ await expect(link).toHaveAttribute('href', href)
+ await expect(link).toHaveAttribute('target', '_blank')
+ await expect(link).toHaveAttribute('rel', /noopener/)
+ }
+ } else {
+ // Docs pages end at Previous / Next, without a footer.
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
+ await expect(footer).toHaveCount(0)
}
- await menu.getByRole('link', { name: 'Tempo EVM', exact: true }).click()
+ await menuButton(page).click()
+ const menu = menuDrawer(page)
+ const areas = menu.getByRole('navigation', { name: 'Products', exact: true })
+ await expect(areas.getByRole('link')).toHaveText(menuLinks)
+ for (const label of menuLinks)
+ await expect(areas.getByRole('link', { name: label, exact: true })).toBeVisible()
+ // Resources is a label above its sections, not a link.
+ await expect(areas.getByText('Resources', { exact: true })).toBeVisible()
+ await expect(areas.getByRole('link', { name: 'Resources', exact: true })).toHaveCount(0)
+ if (path === '/') await expect(areas.locator('[aria-current="page"]')).toHaveCount(0)
+ else await expect(areas.locator('[aria-current="page"]')).toHaveText('Accounts')
+ await expect(menu.getByRole('link', { name: 'Get API key', exact: true })).toHaveAttribute(
+ 'href',
+ 'https://console.tempo.xyz',
+ )
+ await areas.getByRole('link', { name: 'Network', exact: true }).click()
await expect(page).toHaveURL(/\/docs\/development\/?$/)
await expect(menu).toBeHidden()
await expect(page.locator('body')).not.toHaveCSS('overflow', 'hidden')
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
- await drawer
- .locator('summary')
- .filter({ hasText: /^Changelog$/ })
- .click()
+ await expect(breadcrumbItems(page)).toHaveText(['Network', 'Introduction'])
+ await menuButton(page).click()
+ const drawer = sectionPages(page, 'Network')
+ // Changelog is an always-open Network chapter, so its upgrades list without a toggle.
+ await expect(drawer.getByRole('group', { name: 'Changelog', exact: true })).toBeVisible()
await drawer.locator('a[href="/docs/protocol/upgrades/t12"]').click()
await expect(page).toHaveURL(/\/docs\/protocol\/upgrades\/t12\/?$/)
- await expect(drawer).toBeHidden()
+ await expect(menu).toBeHidden()
})
}
for (const path of ['/', '/docs/accounts']) {
- for (const width of [1080, 1440]) {
- test(`desktop header destinations and agent setup stay usable on ${path} at ${width}px`, async ({
+ for (const width of [1080, 1200, 1440]) {
+ test(`desktop top bar products, search and Get API key stay usable on ${path} at ${width}px`, async ({
page,
}) => {
await page.setViewportSize({ width, height: 1000 })
await page.goto(path)
- const navigation = page.getByRole('navigation', { name: 'Developer navigation' })
+ const header = page.locator('header.docs-site-header')
await expect(
- navigation.getByRole('combobox', { name: 'Color theme', includeHidden: true }),
+ header.getByRole('combobox', { name: 'Color theme', includeHidden: true }),
).toHaveCount(0)
- await expect(navigation.getByRole('link', { name: 'Blog', exact: true })).toHaveCount(0)
- const utilities = page.locator('.docs-section-utilities')
- await expect(utilities.getByRole('link')).toHaveText(utilityLabels)
- await expect(utilities.getByRole('link', { name: 'Blog', exact: true })).toHaveCount(0)
- const search = navigation.getByRole('button', { name: 'Search documentation', exact: true })
- const agents = navigation.getByRole('button', { name: 'Agent setup', exact: true })
- await expect(agents).toBeEnabled()
+ await expect(header.getByRole('link', { name: 'Blog', exact: true })).toHaveCount(0)
+ // The products take the bar from 1200px; below it they move into the menu drawer.
+ const wide = width >= 1200
+ const products = productNav(page)
+ const menu = menuButton(page)
+ if (wide) {
+ await expect(products.getByRole('link')).toHaveText(productLabels)
+ await expect(products.getByRole('button')).toHaveText(['Resources'])
+ if (path === '/') await expect(products.locator('[aria-current]')).toHaveCount(0)
+ else await expect(products.locator('[aria-current]')).toHaveText(['Accounts'])
+ await expect(menu).toBeHidden()
+ } else {
+ await expect(products).toBeHidden()
+ await expect(menu).toBeEnabled()
+ }
+ // Get API key is the one primary action; agent setup and Log in left the top bar.
+ await expect(header.getByRole('button', { name: 'Agent setup', exact: true })).toHaveCount(0)
+ await expect(header.getByRole('link', { name: 'Log in', exact: true })).toHaveCount(0)
+ const logo = header.getByRole('link', { name: 'Tempo Developers', exact: true })
+ const search = header.getByRole('button', { name: 'Search documentation', exact: true })
+ const apiKey = header.getByRole('link', { name: 'Get API key', exact: true })
+ await expect(logo).toHaveAttribute('href', '/')
+ await expect(search).toBeEnabled()
+ await expect(apiKey).toHaveAttribute('href', 'https://console.tempo.xyz')
await page.evaluate(() => document.fonts.ready)
+ // The lockup, products, search, Get API key and menu sit left to right, inside the
+ // viewport, without overlap.
+ const controls = wide
+ ? [logo, ...(await products.getByRole('link').all()), resourcesButton(page), search, apiKey]
+ : [logo, search, apiKey, menu]
const boxes = await Promise.all(
- [search, agents].map(async (control) => {
+ controls.map(async (control) => {
await expect(control).toBeVisible()
const box = await control.boundingBox()
if (!box) throw new Error('Header control has no visible bounds')
return box
}),
)
- const [searchBox, agentsBox] = boxes
- expect(searchBox.x).toBeGreaterThan(agentsBox.x + agentsBox.width)
- expect(searchBox.x + searchBox.width).toBeLessThanOrEqual(width)
- for (const [index, a] of boxes.entries()) {
- for (const b of boxes.slice(index + 1)) {
- expect(a.x + a.width <= b.x || b.x + b.width <= a.x).toBe(true)
- }
+ expect(boxes).toHaveLength(wide ? productLabels.length + 4 : 4)
+ for (const [index, box] of boxes.entries()) {
+ expect(box.x + box.width).toBeLessThanOrEqual(width)
+ const previous = boxes[index - 1]
+ if (previous) expect(box.x).toBeGreaterThanOrEqual(previous.x + previous.width)
}
-
- await agents.click()
- const panel = page.locator('#docs-agent-tools')
- await expect(panel).toBeVisible()
- await expect(agents).toHaveAttribute('aria-expanded', 'true')
- const sections = page.getByRole('navigation', { name: 'Documentation sections', exact: true })
- await expect(sections).toBeVisible()
- // Visibility alone does not detect another stacking context covering the panel.
- const overlap = await panel.evaluate((element) => {
- const sectionNav = document.querySelector('.docs-section-nav')
- const panelRect = element.getBoundingClientRect()
- const navRect = sectionNav?.getBoundingClientRect() ?? panelRect
- const left = Math.max(panelRect.left, navRect.left)
- const right = Math.min(panelRect.right, navRect.right)
- const top = Math.max(panelRect.top, navRect.top)
- const bottom = Math.min(panelRect.bottom, navRect.bottom)
- return {
- width: right - left,
- height: bottom - top,
- panelReceivesPointer: element.contains(
- document.elementFromPoint((left + right) / 2, (top + bottom) / 2),
- ),
- }
- })
- expect(overlap.width).toBeGreaterThan(0)
- expect(overlap.height).toBeGreaterThan(0)
- expect(overlap.panelReceivesPointer).toBe(true)
-
- await page.keyboard.press('Escape')
- await expect(panel).toBeHidden()
- await expect(agents).toBeFocused()
- await agents.click()
- await expect(panel).toBeVisible()
- await page.getByRole('heading', { level: 1 }).click({ position: { x: 4, y: 4 } })
- await expect(panel).toBeHidden()
- await expect(agents).toHaveAttribute('aria-expanded', 'false')
})
}
}
-test('agent-first entry page offers setup and payment guides', async ({
+test('entry page offers one start button, three guides, the products and reference links', async ({
page,
- context,
request,
}) => {
- await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await page.setViewportSize({ width: 1440, height: 1000 })
await page.goto('/')
- await expect(page.getByRole('heading', { level: 1 })).toHaveText('Documentation')
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText(
+ 'Build stablecoin payments on Tempo',
+ )
await expect(page.locator('[data-v-gutter-left]')).toBeHidden()
- await expect(page.locator('.tempo-docs-home-product-group')).toHaveCount(6)
+ const home = page.locator('.tempo-docs-home')
+ // One button in the hero.
+ const start = home.locator('.tempo-docs-home-actions').getByRole('link')
+ await expect(start).toHaveText(['Send your first payment'])
+ await expect(start).toHaveAttribute('href', '/get-started/quickstart')
+ // Three guides; Build with AI is the way in to agent setup.
+ const guides = home.locator('.tempo-docs-home-guide')
+ await expect(guides.getByRole('heading', { level: 3 })).toHaveText([
+ 'Send your first payment',
+ 'Create an account',
+ 'Build with AI',
+ ])
+ for (const [label, href] of [
+ ['Run the quickstart', '/get-started/quickstart'],
+ ['Choose a signing method', '/docs/accounts/create'],
+ ['Set up your agent', '/docs/guide/using-tempo-with-ai'],
+ ]) {
+ await expect(guides.getByRole('link', { name: label, exact: true })).toHaveAttribute(
+ 'href',
+ href,
+ )
+ }
+ await expect(home.locator('.tempo-docs-home-product-group')).toHaveCount(6)
for (const product of ['accounts', 'earn', 'routes', 'zones', 'agents', 'development']) {
await expect(
- page.locator(`.tempo-docs-home-product-group h3 a[href="/docs/${product}"]`),
+ home.locator(`.tempo-docs-home-product-group h3 a[href="/docs/${product}"]`),
).toHaveCount(1)
}
- const setup = page.locator('.tempo-agent-start')
- const installStatus = setup.locator('.tempo-agent-start-install').getByRole('status')
- await expect(setup.getByRole('heading', { level: 3 })).toHaveText('Connect Tempo docs')
- await expect(setup.getByRole('button', { name: /Copy prompt/ })).toHaveCount(0)
- await expect(
- setup.getByRole('link', { name: 'All setup options', exact: false }),
- ).toHaveAttribute('href', '/docs/guide/using-tempo-with-ai')
- for (const { label, command, instruction, install } of homeAgentSetups) {
- const multipleCommands = command.includes('\n')
- const commandSuccess =
- label === 'MCP'
- ? 'URL copied. Add it as an HTTP MCP server in your agent’s settings.'
- : multipleCommands
- ? 'Commands copied. Paste them into your terminal and run both commands.'
- : 'Command copied. Paste it into your terminal and run it.'
- await setup.getByRole('radio', { name: label, exact: true }).click()
- await expect(installStatus).toHaveText('')
- const instructionText = setup.locator(
- '.tempo-agent-start-install .tempo-agent-start-instruction',
- )
- if (instruction) await expect(instructionText).toContainText(instruction)
- else await expect(instructionText).toHaveCount(0)
- // The install link sits on the Connect Tempo docs heading row.
- const installLink = setup.locator('.tempo-agent-start-heading .tempo-agent-start-prerequisite')
- if (install) await expect(installLink).toContainText(install)
- else await expect(installLink).toHaveCount(0)
- await setup
- .getByRole('button', {
- name:
- label === 'MCP'
- ? 'Copy URL for MCP'
- : `Copy command${multipleCommands ? 's' : ''} for ${label}`,
- exact: true,
- })
- .click()
- await expect(installStatus).toHaveText(commandSuccess)
- expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(command)
- }
- await setup.getByRole('radio', { name: 'Codex', exact: true }).click()
- await expect(installStatus).toHaveText('')
- await expect(page.locator('.tempo-docs-home')).not.toContainText('Bring your existing EVM app')
+ const reference = home.locator('.tempo-docs-home-reference')
+ await expect(reference.getByRole('heading', { level: 2 })).toHaveText('Reference & tools')
+ expect(
+ await reference
+ .locator('.tempo-docs-home-quick-link a')
+ .evaluateAll((links) => links.map((link) => link.getAttribute('href'))),
+ ).toEqual([
+ '/docs/quickstart/connection-details',
+ '/docs/quickstart/faucet',
+ '/docs/api',
+ '/docs/sdk',
+ ])
+ await expect(home).not.toContainText('Bring your existing EVM app')
for (const anchor of [
'start-here',
'choose-a-build-path',
@@ -583,19 +665,15 @@ test('agent-first entry page offers setup and payment guides', async ({
expect((await request.get(destination)).status(), destination).toBe(200)
}
- await page.getByRole('link', { name: 'Send your first payment', exact: false }).first().click()
+ await start.click()
await page.waitForURL(/\/get-started\/quickstart\/?$/)
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Quickstart')
await expect(
- page
- .getByRole('navigation', { name: 'Documentation sections' })
- .getByRole('link', { name: 'Get Started', exact: true }),
+ productNav(page).getByRole('link', { name: 'Get started', exact: true }),
).toHaveAttribute('aria-current', 'page')
})
-test('agent setup offers manual copy recovery without overflowing a narrow viewport', async ({
- page,
-}) => {
+test('Copy page offers manual recovery without overflowing a narrow viewport', async ({ page }) => {
await page.setViewportSize({ width: 320, height: 844 })
await page.addInitScript(() => {
Object.defineProperty(navigator, 'clipboard', {
@@ -607,51 +685,40 @@ test('agent setup offers manual copy recovery without overflowing a narrow viewp
},
})
})
- await page.goto('/')
- const setup = page.locator('.tempo-agent-start')
- const installStatus = setup.locator('.tempo-agent-start-install').getByRole('status')
- const commands = setup.locator('.tempo-agent-start-command code')
- for (const { label, command } of homeAgentSetups) {
- const multipleCommands = command.includes('\n')
- const commandFailure =
- label === 'MCP'
- ? 'Copy failed. Select and copy the server URL above.'
- : multipleCommands
- ? 'Copy failed. Select and copy both commands above.'
- : 'Copy failed. Select and copy the command above.'
- await setup.getByRole('radio', { name: label, exact: true }).click()
- await expect(installStatus).toHaveText('')
- await setup
- .getByRole('button', {
- name:
- label === 'MCP'
- ? 'Copy URL for MCP'
- : `Copy command${multipleCommands ? 's' : ''} for ${label}`,
- exact: true,
- })
- .click()
- await expect(installStatus).toHaveText(commandFailure)
- await expect(commands).toBeVisible()
- expect(await commands.innerText()).toBe(command)
- expect(
- await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
- label,
- ).toBe(true)
- }
+ await page.goto('/docs/cli')
+ const actions = page.locator('.docs-page-actions')
+ // Below 768px Ask AI and Copy page show only their icons and keep their names.
+ for (const name of ['Ask AI', 'Copy page'])
+ await expect(actions.getByRole('button', { name, exact: true })).toHaveText('')
+ await actions.getByRole('button', { name: 'Copy page', exact: true }).click()
+ await expect(actions.getByRole('status')).toHaveText(
+ 'Could not copy. Open View as Markdown to select the page text.',
+ )
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
+ await actions.getByRole('button', { name: 'More page options', exact: true }).click()
+ const markdown = pageOptionsMenu(page).getByRole('menuitem', {
+ name: 'View as Markdown',
+ exact: true,
+ })
+ await expect(markdown).toBeVisible()
+ await expect(markdown).toHaveAttribute('href', '/assets/md/docs/cli.md')
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
})
-test('mobile documentation menu restores focus and navigates to another section', async ({
- page,
-}) => {
+test('mobile menu restores focus and navigates to another section', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
- await expect(page.getByRole('heading', { level: 1 })).toHaveText('Documentation')
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText(
+ 'Build stablecoin payments on Tempo',
+ )
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
- const trigger = page.getByRole('button', { name: 'Open menu', exact: true })
+ const trigger = menuButton(page)
await trigger.click()
- const menu = page.getByRole('dialog', { name: 'Documentation navigation', exact: true })
+ const menu = menuDrawer(page)
await expect(menu).toBeVisible()
- await expect(menu.getByRole('button', { name: 'Close menu', exact: true })).toBeFocused()
+ // Focus moves into the drawer, which has its own Close button.
+ await expect(menu).toBeFocused()
+ await expect(menu.getByRole('button', { name: 'Close', exact: true })).toBeVisible()
await page.keyboard.press('Escape')
await expect(menu).not.toBeVisible()
await expect(trigger).toBeFocused()
@@ -659,53 +726,56 @@ test('mobile documentation menu restores focus and navigates to another section'
['Earn', '/docs/earn'],
['Accounts', '/docs/accounts'],
['Machine Payments', '/docs/agents'],
- ['Tempo EVM', '/docs/development'],
+ ['Network', '/docs/development'],
]) {
await trigger.click()
await menu
- .getByRole('navigation', { name: 'All documentation sections' })
+ .getByRole('navigation', { name: 'Products', exact: true })
.getByRole('link', { name: label, exact: true })
.click()
await expect(page).toHaveURL(new RegExp(`${path}/?$`))
await expect(menu).toBeHidden()
await expect(page.locator('body')).not.toHaveCSS('overflow', 'hidden')
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
- await expect(drawer.locator(`a[href="${path}"]`)).toBeVisible()
+ await expectSection(page, label, true)
+ await trigger.click()
+ await expect(
+ menu
+ .getByRole('navigation', { name: 'Products', exact: true })
+ .locator('[aria-current="page"]'),
+ ).toHaveText(label)
+ await expect(sectionPages(page, label).locator(`a[href="${path}"]`)).toBeVisible()
await page.keyboard.press('Escape')
- await expect(drawer).toBeHidden()
+ await expect(menu).toBeHidden()
}
})
test('mobile sidebars keep useful product guides visible', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
- for (const [path, destination] of [
- ['/docs/earn', '/docs/earn/how-it-works'],
- ['/docs/routes', '/docs/routes/quotes'],
- ['/docs/zones', '/docs/zones/connect'],
+ for (const [path, section, destination] of [
+ ['/docs/earn', 'Earn', '/docs/earn/how-it-works'],
+ ['/docs/routes', 'Routes', '/docs/routes/quotes'],
+ ['/docs/zones', 'Zones', '/docs/zones/connect'],
]) {
await page.goto(path)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
+ await menuButton(page).click()
+ const drawer = sectionPages(page, section)
await expect(drawer.locator(`a[href="${destination}"]`)).toBeVisible()
- await expect(
- drawer.locator('summary').filter({ hasText: /More about|Earlier testnet/ }),
- ).toHaveCount(0)
+ await expect(drawer.getByRole('button', { name: /More about|Earlier testnet/ })).toHaveCount(0)
await page.keyboard.press('Escape')
}
await page.goto('/docs/accounts')
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
+ await menuButton(page).click()
+ const drawer = sectionPages(page, 'Accounts')
for (const href of ['/docs/accounts/create', '/docs/accounts/fund'])
await expect(drawer.locator(`a[href="${href}"]`)).toBeVisible()
- // Each Features and Build group opens to its own overview page and guides.
+ // Each Features and Build submenu opens to its own overview page and guides.
for (const [group, href] of [
['Account Policies', '/docs/accounts/policies'],
['Access Keys', '/docs/accounts/access-keys'],
['Wallets', '/docs/accounts/wallets'],
]) {
- const disclosure = drawer.locator('summary').filter({ hasText: new RegExp(`^${group}$`) })
+ const disclosure = drawer.getByRole('button', { name: group, exact: true })
await expect(drawer.locator(`a[href="${href}"]`)).toBeHidden()
await disclosure.click()
await expect(drawer.locator(`a[href="${href}"]`)).toBeVisible()
@@ -716,7 +786,7 @@ test('mobile sidebars keep useful product guides visible', async ({ page }) => {
await expect(page).toHaveURL(/\/docs\/accounts\/create\/?$/)
await expect(drawer).toBeHidden()
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Create an Account')
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('Accounts')
+ await expect(breadcrumbItems(page)).toHaveText(['Accounts', 'Create an Account'])
})
test('sandbox guides stay collapsed under Testnet Sandbox until a sandbox page opens', async ({
@@ -724,123 +794,227 @@ test('sandbox guides stay collapsed under Testnet Sandbox until a sandbox page o
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/docs/zones')
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
- const sandbox = drawer.locator('summary').filter({ hasText: /^Testnet Sandbox$/ })
+ await menuButton(page).click()
+ const drawer = sectionPages(page, 'Zones')
+ const sandbox = drawer.getByRole('button', { name: 'Testnet Sandbox', exact: true })
await expect(sandbox).toBeVisible()
await expect(drawer.locator('a[href="/docs/guide/private-zones/deposit-to-a-zone"]')).toBeHidden()
await sandbox.click()
await drawer.locator('a[href="/docs/guide/private-zones"]').click()
await expect(page).toHaveURL(/\/docs\/guide\/private-zones\/?$/)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await menuButton(page).click()
await expect(drawer.locator('a[href="/docs/zones/privacy"]')).toBeVisible()
await expect(
drawer.locator('a[href="/docs/guide/private-zones/deposit-to-a-zone"]'),
).toBeVisible()
await drawer.locator('a[href="/docs/guide/private-zones/deposit-to-a-zone"]').click()
await expect(page).toHaveURL(/\/docs\/guide\/private-zones\/deposit-to-a-zone\/?$/)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await menuButton(page).click()
await expect(
drawer.locator('a[href="/docs/guide/private-zones/withdraw-from-a-zone"]'),
).toBeVisible()
await expect(drawer.locator('a[href="/docs/zones/deposit"]')).toBeVisible()
})
-test('mobile article drawer traps focus and survives client navigation', async ({ page }) => {
+test('mobile menu drawer traps focus and survives client navigation', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/docs/network')
- const trigger = page.getByRole('button', { name: 'Open docs navigation', exact: true })
+ // Below Desktop the menu drawer holds the section's pages, so Vocs' outline row stays hidden.
+ const outline = page
+ .locator('[data-v-outline-mobile]')
+ .getByRole('button', { name: 'On this page', exact: true, includeHidden: true })
+ await expect(outline).toHaveCount(1)
+ await expect(outline).toBeHidden()
+ await expectSection(page, 'Network', true)
+ const trigger = menuButton(page)
await trigger.click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
- const close = drawer.getByRole('button', { name: 'Close docs navigation', exact: true })
- await expect(close).toBeFocused()
- const specifications = drawer.locator('summary').filter({ hasText: /^Changelog$/ })
+ const drawer = menuDrawer(page)
+ const search = drawer.getByRole('button', { name: 'Search docs', exact: true })
+ const close = drawer.getByRole('button', { name: 'Close', exact: true })
+ await expect(drawer).toBeFocused()
+ // Focus wraps between the drawer's last control, Close, and its first, Search docs.
await page.keyboard.press('Shift+Tab')
- await expect(specifications).toBeFocused()
+ await expect(close).toBeFocused()
await page.keyboard.press('Tab')
+ await expect(search).toBeFocused()
+ await page.keyboard.press('Shift+Tab')
await expect(close).toBeFocused()
+ await page.keyboard.press('Tab')
+ await expect(search).toBeFocused()
+ // The products come straight after search.
+ await page.keyboard.press('Tab')
+ await expect(
+ drawer
+ .getByRole('navigation', { name: 'Products', exact: true })
+ .getByRole('link', { name: 'Get started', exact: true }),
+ ).toBeFocused()
await page.keyboard.press('Escape')
await expect(drawer).toBeHidden()
await expect(trigger).toBeFocused()
- await page.getByRole('button', { name: 'Open menu', exact: true }).click()
- const menu = page.getByRole('dialog', { name: 'Documentation navigation', exact: true })
- await menu.getByRole('link', { name: 'API reference', exact: true }).click()
- await expect(page).toHaveURL(/\/docs\/api\/?$/)
- await expect(menu).toBeHidden()
+ // The API reference has no top-bar entry; each product's pages end with its API reference.
await trigger.click()
- await expect(drawer).toBeVisible()
+ await sectionPages(page, 'Network').getByRole('link', { name: 'JSON-RPC', exact: true }).click()
+ await expect(page).toHaveURL(/\/docs\/api\/rpc\/?$/)
+ await expect(drawer).toBeHidden()
+ await trigger.click()
+ // The drawer follows the page to the API section's pages.
+ const api = sectionPages(page, 'API')
+ await expect(api).toBeVisible()
await page.setViewportSize({ width: 1440, height: 1000 })
await expect(drawer).toBeHidden()
await expect(page.locator('body')).not.toHaveCSS('overflow', 'hidden')
})
-test('mobile pages without an outline still expose the documentation drawer', async ({ page }) => {
+test('mobile pages without an outline still expose the menu drawer', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/docs/sdk')
await expect(page.locator('article[data-v-content] h2')).toHaveCount(0)
- const trigger = page.getByRole('button', { name: 'Open docs navigation', exact: true })
+ // The page's generic Overview label takes its group's name.
+ await expect(breadcrumbItems(page)).toHaveText(['SDKs & CLI', 'Tempo SDK'])
+ const trigger = menuButton(page)
await expect(trigger).toBeVisible()
await trigger.click()
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
+ const drawer = menuDrawer(page)
await expect(drawer).toBeVisible()
- // Tempo SDK is an always-open section, so it renders as a heading above its pages.
- await expect(drawer.locator('p').filter({ hasText: /^Tempo SDK$/ })).toBeVisible()
- await drawer.getByRole('link', { name: 'Python', exact: true }).click()
+ await expect(
+ drawer
+ .getByRole('navigation', { name: 'Products', exact: true })
+ .locator('[aria-current="page"]'),
+ ).toHaveText('SDKs & CLI')
+ const pages = sectionPages(page, 'SDKs & CLI')
+ // Tempo SDK is an always-open section, so it renders as a labelled group of its pages.
+ await expect(pages.getByRole('group', { name: 'Tempo SDK', exact: true })).toBeVisible()
+ await pages.getByRole('link', { name: 'Python', exact: true }).click()
await expect(page).toHaveURL(/\/docs\/sdk\/python\/?$/)
await expect(drawer).toBeHidden()
await expect(page.locator('body')).not.toHaveCSS('overflow', 'hidden')
await expect(trigger).toBeVisible()
})
-test('agent setup closes when keyboard focus leaves and when resizing to mobile', async ({
+test('the Copy page menu is keyboard operable, closes on Escape and outside presses, and connects your agent', async ({
page,
}) => {
await page.setViewportSize({ width: 1440, height: 1000 })
await page.goto('/docs/accounts')
- const trigger = page.getByRole('button', { name: 'Agent setup', exact: true })
- const panel = page.locator('#docs-agent-tools')
+ const actions = page.locator('.docs-page-actions')
+ const trigger = actions.getByRole('button', { name: 'More page options', exact: true })
+ const menu = pageOptionsMenu(page)
+ const items = menu.getByRole('menuitem')
+ await expect(trigger).toHaveAttribute('aria-haspopup', 'menu')
+ await expect(trigger).toHaveAttribute('aria-expanded', 'false')
+ await trigger.focus()
+ await page.keyboard.press('Enter')
+ await expect(menu).toBeVisible()
+ await expect(trigger).toHaveAttribute('aria-expanded', 'true')
+ // Copy page is the split button's own action, so the menu holds the other page options.
+ await expect(items).toHaveText(['View as Markdown', 'Connect your agent'])
+ await expect(
+ menu.getByRole('menuitem', { name: 'View as Markdown', exact: true }),
+ ).toHaveAttribute('href', '/assets/md/docs/accounts.md')
+ await expect(items.nth(0)).toBeFocused()
+ await page.keyboard.press('End')
+ await expect(items.nth(1)).toBeFocused()
+ await page.keyboard.press('ArrowDown')
+ await expect(items.nth(0)).toBeFocused()
+ await page.keyboard.press('ArrowUp')
+ await expect(items.nth(1)).toBeFocused()
+ await page.keyboard.press('Home')
+ await expect(items.nth(0)).toBeFocused()
+ await page.keyboard.press('ArrowDown')
+ await expect(items.nth(1)).toBeFocused()
+ // Visibility alone does not detect another stacking context covering the menu.
+ const receivesPointer = await menu.evaluate((element) => {
+ const rect = element.getBoundingClientRect()
+ return element.contains(
+ document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2),
+ )
+ })
+ expect(receivesPointer).toBe(true)
+
+ await page.keyboard.press('Escape')
+ await expect(menu).toBeHidden()
+ await expect(trigger).toBeFocused()
+ await expect(trigger).toHaveAttribute('aria-expanded', 'false')
await trigger.click()
- await expect(panel).toBeVisible()
- await panel.locator('a[href], button:not([disabled])').last().focus()
- await page.keyboard.press('Tab')
- await expect(panel).toBeHidden()
+ await expect(menu).toBeVisible()
+ // While the menu is open, its backdrop takes presses outside it, such as on the H1.
+ const heading = await page.getByRole('heading', { level: 1 }).boundingBox()
+ if (!heading) throw new Error('The page heading has no visible bounds')
+ await page.mouse.click(heading.x + 4, heading.y + 4)
+ await expect(menu).toBeHidden()
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
+ // Agent setup moved from the header into this menu and Get started › Build with AI.
await trigger.click()
- await expect(panel).toBeVisible()
- await page.setViewportSize({ width: 390, height: 844 })
- // Wait for the menu state to close, beyond CSS hiding the desktop header.
- await expect(panel).toHaveCount(0)
+ await menu.getByRole('menuitem', { name: 'Connect your agent', exact: true }).click()
+ await expect(page).toHaveURL(/\/docs\/guide\/using-tempo-with-ai\/?$/)
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText('Build with AI')
+ await expectSection(page, 'Get started')
+})
+
+test('the page header links its section and Ask AI opens the page in ChatGPT or Claude', async ({
+ page,
+}) => {
await page.setViewportSize({ width: 1440, height: 1000 })
- await expect(trigger).toBeVisible()
- await expect(trigger).toHaveAttribute('aria-expanded', 'false')
- await expect(panel).toBeHidden()
+ await page.goto('/docs/accounts/create')
+ const askAi = page
+ .locator('.docs-page-actions')
+ .getByRole('button', { name: 'Ask AI', exact: true })
+ const menu = page.getByRole('menu', { name: 'Ask AI', exact: true })
+ // Ask AI moved from Vocs' floating field into the page header.
+ await expect(page.locator('[data-v-ask-ai-container]')).toBeHidden()
+ await askAi.click()
+ await expect(menu.getByRole('menuitem')).toHaveText(['Ask ChatGPT', 'Ask Claude'])
+ for (const [name, href] of [
+ ['Ask ChatGPT', /^https:\/\/chatgpt\.com\?hints=search&q=\S+$/],
+ ['Ask Claude', /^https:\/\/claude\.ai\/new\?q=\S+$/],
+ ] as const) {
+ const item = menu.getByRole('menuitem', { name, exact: true })
+ await expect(item).toHaveAttribute('href', href)
+ await expect(item).toHaveAttribute('target', '_blank')
+ }
+ await page.keyboard.press('Escape')
+ await expect(menu).toBeHidden()
+ await expect(askAi).toBeFocused()
+ // Cmd/Ctrl+I toggles it.
+ await page.keyboard.press('ControlOrMeta+i')
+ await expect(menu).toBeVisible()
+ await page.keyboard.press('ControlOrMeta+i')
+ await expect(menu).toBeHidden()
+ // The breadcrumb's section links its overview.
+ await expect(breadcrumbItems(page)).toHaveText(['Accounts', 'Create an Account'])
+ await breadcrumbItems(page).getByRole('link', { name: 'Accounts', exact: true }).click()
+ await expect(page).toHaveURL(/\/docs\/accounts\/?$/)
+ await expect(breadcrumbItems(page)).toHaveText(['Accounts', 'Introduction'])
})
-test('article tools copy readable Markdown and survive client navigation', async ({
+test('Copy page copies readable Markdown and survives client navigation', async ({
page,
context,
}) => {
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
await page.goto('/docs/cli')
- const actions = page.getByRole('navigation', { name: 'Page tools' })
- await actions.getByRole('button', { name: 'Copy for agent' }).click()
+ const actions = page.locator('.docs-page-actions')
+ const options = actions.getByRole('button', { name: 'More page options', exact: true })
+ const markdown = pageOptionsMenu(page).getByRole('menuitem', {
+ name: 'View as Markdown',
+ exact: true,
+ })
+ await actions.getByRole('button', { name: 'Copy page', exact: true }).click()
await expect(actions.getByRole('status')).toHaveText('Page Markdown copied to clipboard.')
expect(await page.evaluate(() => navigator.clipboard.readText())).toContain('# Tempo CLI')
- await expect(actions.getByRole('link', { name: 'View Markdown' })).toHaveAttribute(
- 'href',
- '/assets/md/docs/cli.md',
- )
- await page
- .locator('.docs-section-utilities')
- .getByRole('link', { name: 'API reference', exact: true })
- .click()
+ await options.click()
+ await expect(markdown).toHaveAttribute('href', '/assets/md/docs/cli.md')
+ await page.keyboard.press('Escape')
+ await expect(markdown).toBeHidden()
+ // The API reference has no top-bar entry; Get started's sidebar ends with its overview.
+ await productNav(page).getByRole('link', { name: 'Get started', exact: true }).click()
+ await page.waitForURL(/\/get-started\/?$/)
+ await page.locator('[data-v-gutter-left] [data-v-sidebar-container] a[href="/docs/api"]').click()
await page.waitForURL(/\/docs\/api\/?$/)
await expect(actions).toHaveCount(1)
- await expect(actions.getByRole('link', { name: 'View Markdown' })).toHaveAttribute(
- 'href',
- '/assets/md/docs/api.md',
- )
+ await options.click()
+ await expect(markdown).toHaveAttribute('href', '/assets/md/docs/api.md')
})
test('mobile docs search works and appearance persists through navigation and reload', async ({
@@ -848,11 +1022,11 @@ test('mobile docs search works and appearance persists through navigation and re
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/docs/accounts')
- const menu = page.getByRole('dialog', { name: 'Documentation navigation', exact: true })
- await page.getByRole('button', { name: 'Open menu', exact: true }).click()
+ const menu = menuDrawer(page)
+ await menuButton(page).click()
await menu.getByRole('combobox', { name: 'Color theme' }).selectOption('dark')
await expect(page.locator('html')).toHaveAttribute('data-vocs-theme', 'dark')
- await menu.getByRole('button', { name: 'Search documentation', exact: true }).click()
+ await menu.getByRole('button', { name: 'Search docs', exact: true }).click()
await expect(menu).toBeHidden()
const search = page.getByRole('dialog', { name: 'Search documentation', exact: true })
await expect(search).toBeVisible()
@@ -865,7 +1039,7 @@ test('mobile docs search works and appearance persists through navigation and re
await expect(page.locator('html')).toHaveAttribute('data-vocs-theme', 'dark')
await page.reload()
await expect(page.locator('html')).toHaveAttribute('data-vocs-theme', 'dark')
- await page.getByRole('button', { name: 'Open menu', exact: true }).click()
+ await menuButton(page).click()
await menu.getByRole('combobox', { name: 'Color theme' }).selectOption('light')
await expect(page.locator('html')).toHaveAttribute('data-vocs-theme', 'light')
await page.keyboard.press('Escape')
@@ -874,12 +1048,12 @@ test('mobile docs search works and appearance persists through navigation and re
function developerDisclosure(sidebar: Locator, label: string, mobile: boolean) {
return mobile
- ? sidebar.locator('summary:visible').filter({ hasText: new RegExp(`^${label}$`) })
+ ? sidebar.getByRole('button', { name: label, exact: true })
: sidebar.getByRole('button', { name: new RegExp(`^(?:Toggle )?${label}(?: section)?$`) })
}
async function expectOnlyActiveLeaf(sidebar: Locator, path: string) {
- const active = sidebar.locator('a[data-active], a[aria-current="page"]')
+ const active = sidebar.locator(activeSidebarLink)
await expect(active).toHaveCount(1)
await expect(active).toHaveAttribute('href', path)
const hrefs = await sidebar
@@ -950,7 +1124,7 @@ for (const mobile of [false, true]) {
for (const entry of [
{
name: 'token introduction',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/network/tokens',
group: 'TIP-20 Tokens',
sibling: '/docs/guide/issuance/manage-stablecoin',
@@ -958,7 +1132,7 @@ for (const mobile of [false, true]) {
},
{
name: 'token management',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/guide/issuance/manage-stablecoin',
group: 'TIP-20 Tokens',
sibling: '/docs/guide/issuance/mint-stablecoins',
@@ -966,7 +1140,7 @@ for (const mobile of [false, true]) {
},
{
name: 'wallet integration',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/quickstart/wallet-developers',
group: 'Accounts and keys',
sibling: '/docs/development/account-keychain',
@@ -974,7 +1148,7 @@ for (const mobile of [false, true]) {
},
{
name: 'transaction specification',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/protocol/transactions/spec-tempo-transaction',
group: 'Transactions',
sibling: '/docs/guide/tempo-transaction',
@@ -983,7 +1157,7 @@ for (const mobile of [false, true]) {
},
{
name: 'fee specification',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/protocol/fees/spec-fee',
group: 'Fees',
sibling: '/docs/network/fees',
@@ -992,7 +1166,7 @@ for (const mobile of [false, true]) {
},
{
name: 'token specification',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/protocol/tip20/spec',
group: 'TIP-20 Tokens',
sibling: '/docs/network/tokens',
@@ -1001,7 +1175,7 @@ for (const mobile of [false, true]) {
},
{
name: 'account keychain reference',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/protocol/transactions/AccountKeychain',
group: 'Accounts and keys',
sibling: '/docs/development/account-keychain',
@@ -1010,7 +1184,7 @@ for (const mobile of [false, true]) {
},
{
name: 'receive policy reference',
- section: 'Tempo EVM',
+ section: 'Network',
path: '/docs/protocol/tip403/receive-policies',
group: 'Receive payments',
sibling: '/docs/guide/payments/transfer-memos',
@@ -1031,13 +1205,10 @@ for (const mobile of [false, true]) {
}) => {
await page.setViewportSize(viewport)
expect((await page.goto(entry.path))?.status()).toBe(200)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText(
- entry.section,
- )
- if (mobile)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await expectSection(page, entry.section, mobile)
+ if (mobile) await menuButton(page).click()
const sidebar = mobile
- ? page.getByRole('dialog', { name: 'Documentation', exact: true })
+ ? sectionPages(page, entry.section)
: page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
const disclosure = developerDisclosure(sidebar, entry.group, mobile)
await expect(disclosure).toBeVisible()
@@ -1064,47 +1235,37 @@ for (const mobile of [false, true]) {
await expect(sibling).toBeVisible()
if (mobile) await page.keyboard.press('Escape')
expect((await page.reload())?.status()).toBe(200)
- if (mobile)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ if (mobile) await menuButton(page).click()
await expect(sidebar.locator(`a[href="${entry.sibling}"]`)).toBeVisible()
await expectOnlyActiveLeaf(sidebar, entry.path)
if ('guide' in entry && entry.guide) {
- // Guides and their references keep the same tab and chapter through client navigation.
+ // Guides and their references keep the same section and chapter through client navigation.
await sidebar.locator(`a[href="${entry.guide}"]`).click()
await expect(page).toHaveURL(new RegExp(`${entry.guide}/?$`))
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText(
- entry.section,
- )
- if (mobile)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await expectSection(page, entry.section, mobile)
+ if (mobile) await menuButton(page).click()
await expectOnlyActiveLeaf(sidebar, entry.guide)
await expect(disclosure).toBeVisible()
await expect(sidebar.locator(`a[href="${entry.path}"]`)).toBeVisible()
await sidebar.locator(`a[href="${entry.path}"]`).click()
await expect(page).toHaveURL(new RegExp(`${entry.path}/?$`))
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText(
- entry.section,
- )
- if (mobile)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await expectSection(page, entry.section, mobile)
+ if (mobile) await menuButton(page).click()
await expectOnlyActiveLeaf(sidebar, entry.path)
await expect(sidebar.locator(`a[href="${entry.guide}"]`)).toBeVisible()
}
})
}
- test(`${surface} MPP deep links navigate within Machine payments`, async ({ page }) => {
+ test(`${surface} MPP deep links navigate within Machine Payments`, async ({ page }) => {
await page.setViewportSize(viewport)
expect((await page.goto('/docs/guide/machine-payments/pay-as-you-go'))?.status()).toBe(200)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText(
- 'Machine Payments',
- )
- if (mobile)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await expectSection(page, 'Machine Payments', mobile)
+ if (mobile) await menuButton(page).click()
const sidebar = mobile
- ? page.getByRole('dialog', { name: 'Documentation', exact: true })
+ ? sectionPages(page, 'Machine Payments')
: page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
- const active = sidebar.locator('a[data-active], a[aria-current="page"]')
+ const active = sidebar.locator(activeSidebarLink)
await expect(active).toHaveCount(1)
await expect(active).toHaveAttribute('href', '/docs/guide/machine-payments/pay-as-you-go')
await expect(sidebar.getByRole('link', { name: 'Use Mercator', exact: true })).toBeVisible()
@@ -1113,30 +1274,25 @@ for (const mobile of [false, true]) {
await expect(page).toHaveURL(/\/docs\/guide\/machine-payments\/server\/?$/)
if (mobile) await expect(sidebar).toBeHidden()
expect((await page.reload())?.status()).toBe(200)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText(
- 'Machine Payments',
- )
- if (mobile)
- await page.getByRole('button', { name: 'Open docs navigation', exact: true }).click()
+ await expectSection(page, 'Machine Payments', mobile)
+ if (mobile) await menuButton(page).click()
await expect(active).toHaveCount(1)
await expect(active).toHaveAttribute('href', '/docs/guide/machine-payments/server')
})
}
-test('the developer guide catalog opens Browser payments in its Tempo EVM home', async ({
- page,
-}) => {
+test('the developer guide catalog opens Browser payments in its Network home', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 })
await page.goto('/docs/development')
const article = page.locator('article[data-v-content]')
await expect(article.getByRole('heading', { level: 1 })).toHaveText('Tempo EVM')
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('Tempo EVM')
+ await expectSection(page, 'Network')
const browserPayments = article.locator('a[href="/docs/guide/payments/send-a-payment/browser"]')
await expect(browserPayments).toHaveCount(1)
await expect(browserPayments).toHaveText('Browser payments')
await browserPayments.click()
await expect(page).toHaveURL(/\/docs\/guide\/payments\/send-a-payment\/browser\/?$/)
- await expect(page.locator('.docs-section-nav [aria-current="page"]')).toHaveText('Tempo EVM')
+ await expectSection(page, 'Network')
await expect(article.getByRole('heading', { level: 1 })).toHaveText('Browser payments')
const sidebar = page.locator('[data-v-gutter-left] [data-v-sidebar-container]')
await expect(sidebar.getByRole('link', { name: 'Send payments', exact: true })).toBeVisible()
diff --git a/e2e/document-semantics.test.ts b/e2e/document-semantics.test.ts
index 59462c610..37c0f62ab 100644
--- a/e2e/document-semantics.test.ts
+++ b/e2e/document-semantics.test.ts
@@ -82,17 +82,20 @@ test('keeps one route title through docs and OpenAPI client navigation', async (
await page.goto('/docs/quickstart/integrate-tempo', { waitUntil: 'networkidle' })
await expectSingleTitle(page, 'Integrate Tempo with Apps, Wallets, and Services | Tempo Docs')
- await page
- .getByRole('navigation', { name: 'Documentation sections', exact: true })
- .getByRole('link', { name: 'Tempo EVM', exact: true })
- .click()
+ const products = page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
+ // Tempo EVM is now the Network product; its landing page keeps the Tempo EVM title.
+ await products.getByRole('link', { name: 'Network', exact: true }).click()
await page.waitForURL(/\/docs\/development\/?$/)
await expectSingleTitle(page, 'Tempo EVM | Tempo Docs')
- await page
- .locator('.docs-section-utilities')
- .getByRole('link', { name: 'API reference', exact: true })
- .click()
+ // The API reference has no top-bar entry; Get started's sidebar ends with its overview.
+ await products.getByRole('link', { name: 'Get started', exact: true }).click()
+ await page.waitForURL(/\/get-started\/?$/)
+ await expectSingleTitle(page, 'Get Started with Tempo | Tempo Docs')
+
+ await page.locator('[data-v-gutter-left] a[href="/docs/api"]:visible').first().click()
await page.waitForURL(/\/docs\/api\/?$/)
await expectSingleTitle(page, 'Tempo API: Sponsor Gas Fees and Route Stablecoins | Tempo Docs')
diff --git a/e2e/production-mount.test.ts b/e2e/production-mount.test.ts
index eea3134e3..ad096faa2 100644
--- a/e2e/production-mount.test.ts
+++ b/e2e/production-mount.test.ts
@@ -15,10 +15,20 @@ test('production navigation preserves the mount through history and page tools',
const page = await context.newPage()
await page.setViewportSize({ width: 1440, height: 1000 })
await page.goto(`${productionDocsUrl}/get-started/quickstart`)
- const sections = page.getByRole('navigation', { name: 'Documentation sections', exact: true })
+ const products = page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
const sidebar = page.locator('[data-v-gutter-left] nav[data-v-sidebar]')
- await expect(sections.locator('[aria-current]')).toHaveText('Get Started')
- await sections.getByRole('link', { name: 'Accounts', exact: true }).click()
+ await expect(products.locator('[aria-current]')).toHaveText('Get started')
+ await expect(products.getByRole('link', { name: 'Get started', exact: true })).toHaveAttribute(
+ 'href',
+ '/developers/get-started',
+ )
+ // Wait for the header to hydrate (search enables) so the product link is a client navigation.
+ await expect(
+ page.getByRole('button', { name: 'Search documentation', exact: true }),
+ ).toBeEnabled()
+ await products.getByRole('link', { name: 'Accounts', exact: true }).click()
await expect(page).toHaveURL(`${productionDocsUrl}/docs/accounts`)
await sidebar.locator('a[href$="/docs/accounts/create"]').click()
await expect(page).toHaveURL(`${productionDocsUrl}/docs/accounts/create`)
@@ -38,12 +48,15 @@ test('production navigation preserves the mount through history and page tools',
'href',
'/developers/docs/accounts/create',
)
- const tools = page.getByRole('navigation', { name: 'Page tools' })
- await expect(tools).toHaveCount(1)
- await expect(tools.getByRole('link', { name: 'View Markdown' })).toHaveAttribute(
- 'href',
- '/developers/assets/md/docs/accounts/create.md',
- )
+ // One Copy page split button; its menu links the page's Markdown inside the mount.
+ const actions = page.locator('.docs-page-actions')
+ await expect(actions).toHaveCount(1)
+ await actions.getByRole('button', { name: 'More page options', exact: true }).click()
+ await expect(
+ page
+ .getByRole('menu', { name: 'More page options', exact: true })
+ .getByRole('menuitem', { name: 'View as Markdown', exact: true }),
+ ).toHaveAttribute('href', '/developers/assets/md/docs/accounts/create.md')
expect(audit.errors).toEqual([])
expect(audit.failedRequests).toEqual([])
})
@@ -67,7 +80,12 @@ test('production docs search opens the passkey guide with its interactive demo',
`${productionDocsUrl}/docs/accounts/authentication/passkeys#create-a-passkey`,
)
await expect(dialog).toBeHidden()
- await expect(page.locator('.docs-section-nav a[aria-current]')).toHaveText('Accounts')
+ await expect(
+ page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
+ .locator('a[aria-current]'),
+ ).toHaveText('Accounts')
const demo = page.getByTestId('passkey-account-demo')
await expect(demo).toBeVisible()
await expect(
@@ -84,25 +102,37 @@ test('production mobile menus keep the docs mount and current sidebar selection'
const page = await context.newPage()
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${productionDocsUrl}/docs/accounts`)
- await page.getByRole('button', { name: 'Open menu', exact: true }).click()
- const menu = page.getByRole('dialog', { name: 'Documentation navigation', exact: true })
- await menu.getByRole('link', { name: 'Get Started', exact: true }).click()
+ const trigger = page.getByRole('button', { name: 'Open menu', exact: true })
+ // The menu drawer lists the products above the current section's pages.
+ const menu = page.getByRole('dialog', { name: 'Tempo Developers menu', exact: true })
+ const products = menu.getByRole('navigation', { name: 'Products', exact: true })
+ const pages = menu.getByRole('navigation', { name: 'Get started pages', exact: true })
+ await trigger.click()
+ await products.getByRole('link', { name: 'Get started', exact: true }).click()
await expect(page).toHaveURL(`${productionDocsUrl}/get-started`)
await expect(menu).toBeHidden()
await expect(page.locator('body')).not.toHaveCSS('overflow', 'hidden')
- const trigger = page.getByRole('button', { name: 'Open docs navigation', exact: true })
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
await trigger.click()
- await drawer.getByRole('link', { name: 'Quickstart', exact: true }).click()
+ await pages.getByRole('link', { name: 'Quickstart', exact: true }).click()
await expect(page).toHaveURL(`${productionDocsUrl}/get-started/quickstart`)
- await expect(drawer).toBeHidden()
+ await expect(menu).toBeHidden()
+ await expect(
+ page
+ .locator('.docs-page-header')
+ .getByRole('navigation', { name: 'Breadcrumb', exact: true })
+ .getByRole('listitem'),
+ ).toHaveText(['Get started', 'Quickstart'])
await trigger.click()
- await expect(drawer.locator('a[aria-current]')).toHaveAttribute(
+ await expect(products.locator('a[aria-current="page"]')).toHaveAttribute(
+ 'href',
+ '/developers/get-started',
+ )
+ await expect(pages.locator('a[aria-current]')).toHaveAttribute(
'href',
'/developers/get-started/quickstart',
)
await page.keyboard.press('Escape')
- await expect(drawer).toBeHidden()
+ await expect(menu).toBeHidden()
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
expect(audit.errors).toEqual([])
expect(audit.failedRequests).toEqual([])
diff --git a/e2e/sidebar-scroll.test.ts b/e2e/sidebar-scroll.test.ts
index 21ad8dbc3..bd4e10b88 100644
--- a/e2e/sidebar-scroll.test.ts
+++ b/e2e/sidebar-scroll.test.ts
@@ -40,7 +40,8 @@ test('desktop sidebar keeps the current task active through nested steps and cli
await expect(active).toHaveAttribute('href', '/docs/routes')
await page
- .getByRole('navigation', { name: 'Documentation sections', exact: true })
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
.getByRole('link', { name: 'Accounts', exact: true })
.click()
await expect(page).toHaveURL(/\/docs\/accounts\/?$/)
@@ -112,26 +113,31 @@ test('mobile drawer retains the current task after scrolling and follows task na
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${transfer}#track-destination-delivery`)
- const trigger = page.getByRole('button', { name: 'Open docs navigation', exact: true })
- const drawer = page.getByRole('dialog', { name: 'Documentation', exact: true })
+ const trigger = page.getByRole('button', { name: 'Open menu', exact: true })
+ // The menu drawer lists the products above the current section's pages.
+ const drawer = page.getByRole('dialog', { name: 'Tempo Developers menu', exact: true })
+ const products = drawer.getByRole('navigation', { name: 'Products', exact: true })
+ const pages = drawer.getByRole('navigation', { name: 'Routes pages', exact: true })
+ const active = pages.locator('a[aria-current]')
await trigger.click()
- await expect(drawer.locator('a[aria-current]')).toHaveCount(1)
- await expect(drawer.locator('a[aria-current]')).toHaveAttribute('href', transfer)
- await expect(drawer.locator('a[aria-current]')).toHaveAttribute('aria-current', 'page')
- await drawer.locator(`a[href="${deposits}"]`).click()
+ await expect(products.locator('a[aria-current="page"]')).toHaveText('Routes')
+ await expect(active).toHaveCount(1)
+ await expect(active).toHaveAttribute('href', transfer)
+ await expect(active).toHaveAttribute('aria-current', 'page')
+ await pages.locator(`a[href="${deposits}"]`).click()
await expect(drawer).toBeHidden()
await expect(page).toHaveURL(new RegExp(`${deposits}/?$`))
await page
.locator('[id="track-each-deposit"]')
.evaluate((element) => element.scrollIntoView({ behavior: 'instant' }))
await trigger.click()
- await expect(drawer.locator('a[aria-current]')).toHaveCount(1)
- await expect(drawer.locator('a[aria-current]')).toHaveAttribute('href', deposits)
- await drawer.locator('a[href="/docs/routes"]').click()
+ await expect(active).toHaveCount(1)
+ await expect(active).toHaveAttribute('href', deposits)
+ await pages.locator('a[href="/docs/routes"]').click()
await expect(drawer).toBeHidden()
await trigger.click()
- await expect(drawer.locator('a[aria-current]')).toHaveAttribute('href', '/docs/routes')
- await expect(drawer.locator('a[aria-current]')).toHaveAttribute('aria-current', 'page')
+ await expect(active).toHaveAttribute('href', '/docs/routes')
+ await expect(active).toHaveAttribute('aria-current', 'page')
})
test('OpenAPI endpoints retain their own active state after client navigation from a task', async ({
@@ -154,10 +160,12 @@ test('OpenAPI endpoints retain their own active state after client navigation fr
await expect(sidebar.locator('a[data-active]')).toHaveCount(1)
await expect(sidebar.locator('a[data-active]')).toHaveAttribute('href', href)
}
- await page
- .getByRole('navigation', { name: 'Documentation sections', exact: true })
- .getByRole('link', { name: 'Routes', exact: true })
- .click()
+ // The API reference has no top-bar entry, so no product is current; Routes opens from the bar.
+ const products = page
+ .locator('header.docs-site-header')
+ .getByRole('navigation', { name: 'Products', exact: true })
+ await expect(products.locator('[aria-current]')).toHaveCount(0)
+ await products.getByRole('link', { name: 'Routes', exact: true }).click()
await expect(page).toHaveURL(/\/docs\/routes\/?$/)
await expect(sidebar.locator('a[data-active]')).toHaveCount(1)
await expect(sidebar.locator('a[data-active]')).toHaveAttribute('href', '/docs/routes')
diff --git a/e2e/zyzz-styles.test.ts b/e2e/zyzz-styles.test.ts
index b3f73d112..9e11260bd 100644
--- a/e2e/zyzz-styles.test.ts
+++ b/e2e/zyzz-styles.test.ts
@@ -47,7 +47,8 @@ for (const width of [390, 1440]) {
.poll(() =>
page.locator('body').evaluate((body) => {
const style = getComputedStyle(body)
- return { font: style.fontFamily.includes('Pilat'), margin: style.margin }
+ // Docs pages set copy in Inter (DS Body); titles keep Pilat.
+ return { font: style.fontFamily.includes('Inter'), margin: style.margin }
}),
)
.toEqual({ font: true, margin: '0px' })
@@ -61,7 +62,7 @@ for (const width of [390, 1440]) {
.trim(),
),
)
- .toBe('65px')
+ .toBe('56px')
await expect(page.locator('.docs-site-header')).toHaveCSS('position', 'fixed')
await expect(page.locator('html')).toHaveCSS('color-scheme', theme)
await expect
@@ -96,7 +97,8 @@ test('compiled homepage layout responds without losing its brand typography', as
guides.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length),
)
.toBe(1)
- await expect(page.locator('.tempo-docs-home h1')).toHaveCSS('font-weight', '500')
+ // Headings are regular weight (system-v2).
+ await expect(page.locator('.tempo-docs-home h1')).toHaveCSS('font-weight', '400')
})
test('component recipes preserve badges and keyboard-accessible docs buttons', async ({ page }) => {
@@ -104,7 +106,8 @@ test('component recipes preserve badges and keyboard-accessible docs buttons', a
const badge = page.locator('.docs-specification-meta span').first()
// Inline flex is blockified inside the specification's flex link.
await expect(badge).toHaveCSS('display', 'flex')
- await expect(badge).toHaveCSS('border-radius', '8px')
+ // Badges take radius 4 (system-v2).
+ await expect(badge).toHaveCSS('border-radius', '4px')
await page.goto('/docs/guide/using-tempo-with-ai')
await selectTab(page.getByRole('tab', { name: 'Cursor', exact: true }))
@@ -170,21 +173,22 @@ test('closed mobile navigation stays outside the viewport and restores focus on
await page.setViewportSize({ width: 390, height: 844 })
await page.emulateMedia({ reducedMotion: 'reduce' })
await page.goto('/docs/guide/issuance/create-a-stablecoin')
- const trigger = page.getByRole('button', { name: 'Open docs navigation', exact: true })
- // Include the closed dialog: an ARIA-hidden element can still cover the page visually.
+ const trigger = page.getByRole('button', { name: 'Open menu', exact: true })
+ // Include a closed dialog: an ARIA-hidden element can still cover the page visually. The
+ // closed menu drawer unmounts, so nothing is left to cover the page.
const panel = page.getByRole('dialog', {
- name: 'Documentation',
+ name: 'Tempo Developers menu',
exact: true,
includeHidden: true,
})
- const rightEdge = () => panel.evaluate((element) => element.getBoundingClientRect().right)
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
- await expect.poll(rightEdge).toBeLessThanOrEqual(0)
+ await expect(panel).toHaveCount(0)
await trigger.click()
await expect(panel).toBeInViewport()
- await expect(panel).toHaveAttribute('aria-modal', 'true')
+ // The drawer is modal: it hides the page behind it, top bar included, from assistive technology.
+ await expect(page.locator('header.docs-site-header')).toHaveAttribute('aria-hidden', 'true')
await page.keyboard.press('Escape')
- await expect.poll(rightEdge).toBeLessThanOrEqual(0)
+ await expect(panel).toHaveCount(0)
await expect(trigger).toBeFocused()
// Activation need not focus the button (for example, on touch browsers).
@@ -193,8 +197,8 @@ test('closed mobile navigation stays outside the viewport and restores focus on
element.click()
})
await expect(panel).toBeInViewport()
- await panel.getByRole('button', { name: 'Close docs navigation', exact: true }).click()
- await expect.poll(rightEdge).toBeLessThanOrEqual(0)
+ await panel.getByRole('button', { name: 'Close', exact: true }).click()
+ await expect(panel).toHaveCount(0)
await expect(trigger).toBeFocused()
})
diff --git a/public/SKILL.md b/public/SKILL.md
index cc6f688da..d77ec4a0e 100644
--- a/public/SKILL.md
+++ b/public/SKILL.md
@@ -16,23 +16,25 @@ 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. |
-| [API reference](https://tempo.xyz/developers/docs/api.md) | Find Tempo API endpoints by product, authentication, Console management endpoints, JSON-RPC, and the indexer. |
+| [Network](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; run RPC, standby, or validator nodes, upgrade node software, or follow network upgrades, activation timelines, and node releases. |
+| [API](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; Network covers payments, the chain capabilities behind them, and running nodes. Routes covers cross-network movement. The API section 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 Network, 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/components/Badge.tsx b/src/components/Badge.tsx
index 67c79c3ea..79a945043 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -13,11 +13,12 @@ export function Badge({
const badge = variants({
base: {
display: 'inline-flex',
- minHeight: '24px',
+ minHeight: '20px',
alignItems: 'center',
justifyContent: 'center',
- borderRadius: tokens.radius.md,
- paddingInline: tokens.spacing['2'],
+ // Badges (20 high) take radius 4.
+ borderRadius: tokens.radius.sm,
+ paddingInline: tokens.spacing['1_5'],
textAlign: 'center',
fontWeight: tokens.fontWeight.medium,
fontSize: tokens.fontSize.xs,
@@ -52,9 +53,8 @@ const badge = variants({
minWidth: '80px',
minHeight: '28px',
paddingInline: tokens.spacing['2'],
- // design-exception: TDS Badge small radius (6px), between the 4px and 8px steps.
- borderRadius: '6px !custom',
- '--corner-radius': '6px',
+ borderRadius: tokens.radius.sm,
+ '--corner-radius': tokens.radius.sm,
boxSizing: 'border-box',
// A real border (not TDS's overlay) so the smooth-corner fallback can stroke it.
borderWidth: tokens.borderWidth.hairline,
diff --git a/src/components/DocsCards.styles.ts b/src/components/DocsCards.styles.ts
index cd1ff3b85..32189d227 100644
--- a/src/components/DocsCards.styles.ts
+++ b/src/components/DocsCards.styles.ts
@@ -2,15 +2,18 @@ import { global } from 'zyzz/web'
import { inherited } from '../styles/inherited'
import { vars as tokens } from '../styles/theme'
-// Vocs boxes the Previous/Next pagination links below its sm breakpoint. Keep them plain links
-// at every width; this layer follows vocs_utilities so it outranks the max-sm utilities.
+// Vocs boxes the Previous/Next pagination links below its sm breakpoint. Keep them as the same
+// next-step cards at every width; this layer follows vocs_utilities so it outranks max-sm.
global({
'@layer utilities': {
'@media (width < 640px)': {
'[data-v-pagination] > a': {
- padding: 0,
- border: 0,
- borderRadius: 0,
+ paddingBlock: tokens.spacing['4'],
+ paddingInline: tokens.spacing['5'],
+ borderWidth: tokens.borderWidth.hairline,
+ borderStyle: 'solid',
+ borderColor: tokens.color.hairline,
+ borderRadius: tokens.radius.lg,
},
},
},
@@ -20,13 +23,9 @@ global({
// Vocs pagination links share the card markup, so they are excluded explicitly.
global({
':root': {
- '--tempo-card-radius': tokens.radius.panel,
- '--tempo-card-padding': '28px',
- },
- '@media (width < 700px)': {
- ':root': {
- '--tempo-card-padding': '24px',
- },
+ // Cards are surfaces: radius 12, padding 20 (system-v2).
+ '--tempo-card-radius': tokens.radius.lg,
+ '--tempo-card-padding': '20px',
},
'article[data-v-content] a[class~="vocs:flex-col"]:not([data-v-pagination] > a):has(> [class~="vocs:text-heading"]):has( > [class~="vocs:text-secondary"] )':
{
@@ -34,16 +33,20 @@ global({
gridTemplateColumns: 'minmax(0, 1fr)',
gridTemplateRows: 'auto 1fr',
alignContent: 'start',
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- gap: '12px 10px !custom',
+ // Title to description 4; the icon column sits 8 from the title.
+ rowGap: tokens.spacing['1'],
+ columnGap: tokens.spacing['2'],
minWidth: 0,
// design-exception: Preserve the inherited component/framework scope at the point of use.
padding: 'var(--tempo-card-padding) !custom',
- border: 0,
+ // Surfaces sit on the page background with a 1px hairline.
+ borderWidth: tokens.borderWidth.hairline,
+ borderStyle: 'solid',
+ borderColor: tokens.color.hairline,
// design-exception: Preserve the inherited component/framework scope at the point of use.
borderRadius: 'var(--tempo-card-radius) !custom',
'--corner-radius': 'var(--tempo-card-radius)',
- backgroundColor: tokens.color.panel,
+ backgroundColor: tokens.color.background,
color: inherited.color.vocsTextColorPrimary,
textDecoration: 'none',
@@ -77,11 +80,11 @@ global({
':is(article[data-v-content] a[class~="vocs:flex-col"]:not([data-v-pagination] > a):has(> [class~="vocs:text-heading"]):has( > [class~="vocs:text-secondary"] )) > [class~="vocs:text-heading"]':
{
gridRow: 1,
- fontSize: tokens.fontSize.lead,
+ fontSize: tokens.fontSize.body,
fontWeight: tokens.fontWeight.medium,
- letterSpacing: tokens.letterSpacing.tight,
- lineHeight: tokens.lineHeight.snug,
+ letterSpacing: tokens.letterSpacing.normal,
+ lineHeight: tokens.lineHeight.body,
},
':is(article[data-v-content] a[class~="vocs:flex-col"]:not([data-v-pagination] > a):has(> [class~="vocs:text-heading"]):has( > [class~="vocs:text-secondary"] )) > [class~="vocs:text-secondary"]':
{
@@ -90,7 +93,7 @@ global({
color: inherited.color.vocsTextColorSecondary,
fontSize: tokens.fontSize.sm,
- lineHeight: tokens.lineHeight.relaxed,
+ lineHeight: tokens.lineHeight.control,
},
':is(article[data-v-content] a[class~="vocs:flex-col"]:not([data-v-pagination] > a):has(> [class~="vocs:text-heading"]):has( > [class~="vocs:text-secondary"] )):focus-visible':
{
diff --git a/src/components/DocsFooter.recipes.ts b/src/components/DocsFooter.recipes.ts
new file mode 100644
index 000000000..7170fc42b
--- /dev/null
+++ b/src/components/DocsFooter.recipes.ts
@@ -0,0 +1,139 @@
+import { style } from '../styles/recipes'
+import { vars as tokens } from '../styles/theme'
+
+// The footer (Home and Not found) sits under one top hairline, with its text on the page
+// gutter (24, 16 below Tablet). The fill hides the outline rail where the footer runs under it.
+export const root = style({
+ paddingInline: tokens.spacing['4'],
+ borderTopWidth: tokens.borderWidth.hairline,
+ borderTopStyle: 'solid',
+ borderTopColor: tokens.color.hairline,
+ backgroundColor: tokens.color.background,
+ fontFamily: tokens.fontFamily.ui,
+ letterSpacing: tokens.letterSpacing.normal,
+ color: tokens.color.muted,
+ '@media (width >= 768px)': {
+ paddingInline: tokens.spacing['6'],
+ },
+})
+
+// Full (Home and Not found) sits on the panel fill (DS background/primary), as in the Figma.
+export const full = style({
+ backgroundColor: tokens.color.panel,
+})
+
+export const inlineList = style({
+ display: 'flex',
+ flexWrap: 'wrap',
+ alignItems: 'center',
+ columnGap: tokens.spacing['4'],
+ rowGap: tokens.spacing['2'],
+})
+
+// The T mark and the tagline, then three columns of 14/20 links, then the 12/16 bottom row.
+export const brand = style({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: tokens.spacing['4'],
+ paddingBlockStart: tokens.spacing['12'],
+})
+
+export const mark = style({
+ width: '17px',
+ height: '18px',
+ color: tokens.color.foreground,
+})
+
+export const tagline = style({
+ maxWidth: '320px',
+ margin: tokens.spacing['0'],
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ color: tokens.color.muted,
+})
+
+export const columns = style({
+ display: 'grid',
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ columnGap: tokens.spacing['6'],
+ rowGap: tokens.spacing['8'],
+ paddingBlockStart: tokens.spacing['8'],
+ paddingBlockEnd: tokens.spacing['12'],
+ '@media (width >= 768px)': {
+ gridTemplateColumns: 'repeat(3, minmax(0, 160px))',
+ },
+})
+
+export const column = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: tokens.spacing['4'],
+ minWidth: 0,
+})
+
+export const columnHeader = style({
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.medium,
+ lineHeight: tokens.lineHeight.control,
+ color: tokens.color.foreground,
+})
+
+export const list = style({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: tokens.spacing['3'],
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+})
+
+export const bottom = style({
+ display: 'flex',
+ flexWrap: 'wrap',
+ alignItems: 'center',
+ columnGap: tokens.spacing['4'],
+ rowGap: tokens.spacing['2'],
+ paddingBlock: tokens.spacing['6'],
+ borderTopWidth: tokens.borderWidth.hairline,
+ borderTopStyle: 'solid',
+ borderTopColor: tokens.color.hairline,
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
+})
+
+// Links are secondary and turn primary on hover.
+export const link = style({
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: tokens.spacing['0_5'],
+ color: tokens.color.muted,
+ textDecoration: 'none',
+ transitionProperty: 'color, background-color, border-color, opacity, transform',
+ transitionDuration: 'var(--tempo-exit)',
+ transitionTimingFunction: 'var(--tempo-ease)',
+ selectors: {
+ '&:hover': {
+ '@media (hover: hover)': {
+ color: tokens.color.foreground,
+ transitionDuration: 'var(--tempo-enter)',
+ },
+ },
+ },
+ '@media (prefers-reduced-motion: reduce)': {
+ transitionProperty: 'none',
+ },
+})
+
+export const externalIcon = style({
+ flexShrink: 0,
+})
+
+export const visuallyHidden = style({
+ position: 'absolute',
+ width: '1px',
+ height: '1px',
+ overflow: 'hidden',
+ clipPath: 'inset(50%)',
+ whiteSpace: 'nowrap',
+})
diff --git a/src/components/DocsFooter.tsx b/src/components/DocsFooter.tsx
new file mode 100644
index 000000000..7c132bbd7
--- /dev/null
+++ b/src/components/DocsFooter.tsx
@@ -0,0 +1,113 @@
+'use client'
+
+import { MdxPageContext } from 'vocs'
+import { Link, useRouter } from 'waku'
+import ArrowUpRightIcon from '~icons/lucide/arrow-up-right'
+import { docsResourceSections, docsSections } from '../lib/docs-sections'
+import * as ui from './DocsFooter.recipes'
+import { TempoMark } from './ToolLogos'
+
+type FooterLink = { label: string; href: string }
+type FooterColumn = { header: string; links: FooterLink[] }
+
+const githubLink: FooterLink = { label: 'GitHub', href: 'https://github.com/tempoxyz' }
+const xLink: FooterLink = { label: 'X', href: 'https://twitter.com/tempo' }
+const tempoLink: FooterLink = { label: 'tempo.xyz', href: 'https://tempo.xyz' }
+const consoleLink: FooterLink = { label: 'Console', href: 'https://console.tempo.xyz' }
+const explorerLink: FooterLink = { label: 'Explorer', href: 'https://explore.tempo.xyz' }
+
+const socialLinks = [githubLink, xLink]
+
+// Products and Resources repeat the top bar, with its labels and landing routes; the API
+// reference, which has no top-bar entry, closes Resources.
+const columns: FooterColumn[] = [
+ {
+ header: 'Products',
+ links: docsSections.map((section) => ({ label: section.label, href: section.href })),
+ },
+ {
+ header: 'Resources',
+ links: [
+ ...docsResourceSections.map((section) => ({ label: section.label, href: section.href })),
+ { label: 'API reference', href: '/docs/api' },
+ ],
+ },
+ {
+ header: 'Tempo',
+ links: [
+ tempoLink,
+ consoleLink,
+ { label: 'Wallet', href: 'https://wallet.tempo.xyz' },
+ explorerLink,
+ { label: 'MPP', href: 'https://mpp.dev' },
+ { label: 'Mercator', href: 'https://mercator.sh' },
+ ],
+ },
+]
+
+function FooterLinkItem({ link }: { link: FooterLink }) {
+ if (link.href.startsWith('/'))
+ return (
+
+ {link.label}
+
+ )
+ return (
+
+ {link.label}
+
+ (opens in a new tab)
+
+ )
+}
+
+function LinkList({ links, className }: { links: FooterLink[]; className: string }) {
+ return (
+
+ {links.map((link) => (
+
+
+
+ ))}
+
+ )
+}
+
+/** Home and Not found (its frontmatter sets `footer: full`) have a footer; docs pages end at Previous / Next. */
+function useShowsFooter() {
+ const { path } = useRouter()
+ const { frontmatter } = MdxPageContext.use()
+ const pathname = (path ?? '/').replace(/^\/developers(?=\/|$)/, '').replace(/\/+$/, '') || '/'
+ return pathname === '/' || frontmatter?.footer === 'full'
+}
+
+/** The site footer on Home and Not found, rendered through the Vocs Footer slot. */
+export default function DocsFooter() {
+ const showsFooter = useShowsFooter()
+ if (!showsFooter) return null
+ const copyright = © {new Date().getFullYear()} Tempo
+
+ return (
+
+
+
+
Stablecoin payments infrastructure for developers, apps and agents.
+
+
+ {columns.map((column) => (
+
+ ))}
+
+
+ {copyright}
+
+
+
+ )
+}
diff --git a/src/components/DocsHeader.recipes.ts b/src/components/DocsHeader.recipes.ts
deleted file mode 100644
index c8c47e7b2..000000000
--- a/src/components/DocsHeader.recipes.ts
+++ /dev/null
@@ -1,440 +0,0 @@
-import { inherited } from '../styles/inherited'
-import { style } from '../styles/recipes'
-import { style as instanceStyle } from '../styles/scoped'
-import { vars as tokens } from '../styles/theme'
-export const tempoLogoText = style({
- display: 'block',
- backgroundColor: 'currentcolor !custom',
-})
-export const activeSquareIcon = style({
- width: '11px',
- height: '11px',
- flexShrink: 0,
-
- color: inherited.color.colorMixInOklabForeground70Transparent,
-})
-export const searchIconIcon = style({
- flexShrink: 0,
-})
-export const chevronIcon = style({
- flexShrink: 0,
- opacity: '60%',
- transitionProperty: 'transform, translate, scale, rotate',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '200ms',
-})
-export const chevronIcon2 = style({
- rotate: '180deg',
-})
-export const codexLogo = style({
- width: tokens.spacing['3_5'],
- height: tokens.spacing['3_5'],
- flexShrink: 0,
-})
-export const commandTabsLayout = style({
- display: 'flex',
- flexWrap: 'wrap',
- gap: tokens.spacing['1_5'],
-})
-export const commandTabsButton = style({
- display: 'inline-flex',
- alignItems: 'center',
- gap: tokens.spacing['1_5'],
- borderRadius: tokens.radius.sm,
- paddingInline: tokens.spacing['2_5'],
- paddingBlock: tokens.spacing['1_5'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.xs,
- letterSpacing: tokens.letterSpacing.normal,
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
-})
-export const commandTabsButton2 = style({
- backgroundColor: inherited.color.colorMixInOklabForeground6Transparent,
- color: tokens.color.foreground,
-})
-export const commandTabsButton3 = style({
- color: inherited.color.colorMixInOklabForeground60Transparent,
- selectors: {
- '&:hover': {
- '@media (hover: hover)': {
- backgroundColor: inherited.color.colorMixInOklabForeground3Transparent,
-
- color: inherited.color.colorMixInOklabForeground70Transparent,
- },
- },
- },
-})
-export const commandSnippetButton = style({
- display: 'flex',
- minHeight: '48px',
- width: '100%',
- alignItems: 'flex-start',
- gap: tokens.spacing['3'],
- borderRadius: tokens.radius.sm,
-
- backgroundColor: inherited.color.colorMixInOklabForeground35000000000000004Transparent,
- paddingInline: tokens.spacing['3'],
- paddingBlock: tokens.spacing['2_5'],
- textAlign: 'left',
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
- selectors: {
- '&:hover': {
- '@media (hover: hover)': {
- backgroundColor: inherited.color.colorMixInOklabForeground6Transparent,
- },
- },
- },
-})
-export const code = style({
- display: 'grid',
- minWidth: '0',
- flex: '1 1 0%',
- gridTemplateColumns: 'auto minmax(0,1fr)',
- gap: tokens.spacing['2'],
- fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.xs,
-
- lineHeight: tokens.lineHeight.normal,
- overflowWrap: 'break-word',
- whiteSpace: 'pre-wrap',
- color: tokens.color.foreground,
-})
-export const commandSnippetText = style({
- color: inherited.color.colorMixInOklabForeground55Transparent,
- WebkitUserSelect: 'none',
- userSelect: 'none',
-})
-export const commandSnippetText2 = style({
- minWidth: '0',
-})
-export const commandSnippetText3 = style({
- marginTop: tokens.spacing['1'],
- flexShrink: 0,
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
-})
-export const commandSnippetText4 = style({
- color: tokens.color.foreground,
-})
-export const commandSnippetText5 = style({
- color: inherited.color.colorMixInOklabForeground55Transparent,
- selectors: {
- '&:is(:where(.group\\/copy):hover *)': {
- '@media (hover: hover)': {
- color: inherited.color.colorMixInOklabForeground70Transparent,
- },
- },
- },
-})
-export const agentCommandSectionLayout = style({
- borderRadius: tokens.radius.sm,
- paddingInline: tokens.spacing['3'],
- paddingBlock: tokens.spacing['2_5'],
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
- selectors: {
- '&:hover': {
- '@media (hover: hover)': {
- backgroundColor: inherited.color.colorMixInOklabForeground4Transparent,
- },
- },
- },
-})
-export const agentCommandSectionLayout2 = style({
- display: 'flex',
- alignItems: 'flex-start',
- gap: tokens.spacing['3'],
-})
-export const agentCommandSectionText = style({
- display: 'grid',
- width: '34px',
- height: '34px',
- flexShrink: 0,
- placeItems: 'center',
-
- backgroundColor: inherited.color.surfaceInput,
- color: tokens.color.foreground,
-})
-export const anchor = style({
- position: 'relative',
- display: 'flex',
- minWidth: '0',
- flexDirection: 'column',
- gap: tokens.spacing['0_5'],
- paddingInlineEnd: tokens.spacing['5'],
-})
-export const arrowUpRight = style({
- position: 'absolute',
- top: tokens.spacing['0_5'],
- insetInlineEnd: '0',
- width: tokens.spacing['3'],
- height: tokens.spacing['3'],
-
- color: inherited.color.colorMixInOklabForeground55Transparent,
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
- selectors: {
- '&:is(:where(.group\\/item):hover *)': {
- '@media (hover: hover)': {
- color: inherited.color.colorMixInOklabForeground60Transparent,
- },
- },
- },
-})
-export const agentCommandSectionText2 = style({
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.sm,
- letterSpacing: tokens.letterSpacing.normal,
- color: tokens.color.foreground,
-})
-export const agentCommandSectionText3 = style({
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.compact,
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.normal,
-
- color: inherited.color.colorMixInOklabForeground60Transparent,
-})
-export const agentCommandSectionLayout3 = style({
- marginTop: tokens.spacing['3'],
-
- marginInlineStart: tokens.spacing['12'],
- selectors: {
- ':where(& > :not(:last-child))': {
- '--tempo-style-space-y-reverse': '0',
-
- marginBlockStart: 0,
-
- marginBlockEnd: tokens.spacing['3'],
- },
- },
-})
-export const agentsPanelLayout = style({
- width: '520px',
- padding: tokens.spacing['3'],
-})
-export const agentsPanelLayout2 = style({
- paddingBottom: tokens.spacing['4'],
- paddingInlineStart: tokens.spacing['3'],
-})
-export const agentsPanelDescription = style({
- paddingInline: tokens.spacing['3'],
- paddingTop: tokens.spacing['2'],
- paddingBottom: tokens.spacing['1_5'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.compact,
- letterSpacing: tokens.letterSpacing.normal,
-
- color: inherited.color.colorMixInOklabForeground55Transparent,
-})
-export const agentsPanelLayout3 = style({
- selectors: {
- ':where(& > :not(:last-child))': {
- '--tempo-style-space-y-reverse': '0',
-
- marginBlockStart: 0,
-
- marginBlockEnd: tokens.spacing['1'],
- },
- },
-})
-export const agentsPanelDescription2 = style({
- paddingInline: tokens.spacing['2_5'],
-
- paddingBlock: tokens.spacing['1'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.xs,
-
- color: inherited.color.colorMixInOklabForeground60Transparent,
-})
-export const anchor2 = style({
- // design-exception: Derive this layout value from the existing responsive CSS variables.
- marginInline: 'calc(var(--spacing) * -2) !custom',
- display: 'flex',
- minHeight: tokens.spacing['8'],
- alignItems: 'center',
- gap: tokens.spacing['2'],
- // SB3 in the drawer: the desktop sidebar's smoothed radius and hover motion.
- borderRadius: tokens.radius.md,
- '--corner-radius': tokens.radius.md,
- paddingInline: tokens.spacing['2'],
-
- paddingBlock: tokens.spacing['1'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.sm,
- letterSpacing: tokens.letterSpacing.normal,
- transitionProperty: 'color, background-color, border-color, opacity, transform',
- transitionTimingFunction: 'var(--tempo-ease)',
- transitionDuration: 'var(--tempo-exit)',
- selectors: {
- '&:hover': { transitionDuration: 'var(--tempo-enter)' },
- },
-})
-// SB1 in the drawer: the current page does not change on hover.
-export const anchor3 = style({
- fontWeight: tokens.fontWeight.medium,
- color: tokens.color.foreground,
-})
-// SB2 in the drawer: entries use the primary color.
-export const anchor4 = style({
- color: tokens.color.foreground,
- selectors: {
- '&:hover': {
- '@media (hover: hover)': {
- backgroundColor: inherited.color.colorMixInOklabForeground4Transparent,
- },
- },
- },
-})
-export const arrowUpRight2 = style({
- marginTop: tokens.spacing['0_5'],
- width: tokens.spacing['3'],
- height: tokens.spacing['3'],
-})
-export const sidebarDisclosureDetails = style({
- marginTop: tokens.spacing['1'],
-})
-export const sidebarDisclosureSummary = style({
- // design-exception: Derive this layout value from the existing responsive CSS variables.
- marginInline: 'calc(var(--spacing) * -2) !custom',
- display: 'flex',
- minHeight: tokens.spacing['8'],
- cursor: 'pointer',
- listStyleType: 'none',
- alignItems: 'center',
- justifyContent: 'space-between',
- borderRadius: tokens.radius.md,
- '--corner-radius': tokens.radius.md,
- paddingInline: tokens.spacing['2'],
-
- paddingBlock: tokens.spacing['1'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.sm,
- letterSpacing: tokens.letterSpacing.normal,
-
- color: tokens.color.foreground,
- transitionProperty: 'color, background-color, border-color, opacity, transform',
- transitionTimingFunction: 'var(--tempo-ease)',
- transitionDuration: 'var(--tempo-exit)',
- selectors: {
- '&:hover': {
- transitionDuration: 'var(--tempo-enter)',
- '@media (hover: hover)': {
- backgroundColor: inherited.color.colorMixInOklabForeground4Transparent,
- },
- },
- '&::-webkit-details-marker': {
- display: 'none',
- },
- },
-})
-export const sidebarDisclosureLayout = style({
- marginTop: tokens.spacing['1'],
- marginInlineStart: tokens.spacing['2'],
- display: 'flex',
- flexDirection: 'column',
- gap: tokens.spacing['0'],
- borderInlineStartStyle: 'solid',
- borderInlineStartWidth: tokens.borderWidth.hairline,
- borderColor: tokens.color.line,
- paddingInlineStart: tokens.spacing['3'],
-})
-export const sidebarNodesLayout = style({
- marginTop: tokens.spacing['5'],
- selectors: {
- '&:first-child': {
- marginTop: tokens.spacing['0'],
- },
- },
-})
-export const sidebarNodesLayout2 = style({
- marginTop: tokens.spacing['3'],
-})
-// SB2 in the drawer: group labels take the entry type in the secondary color.
-export const sidebarNodesDescription = style({
- // design-exception: Derive this layout value from the existing responsive CSS variables.
- marginInline: 'calc(var(--spacing) * -2) !custom',
- paddingInline: tokens.spacing['2'],
- paddingBlock: tokens.spacing['1'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.sm,
- lineHeight: tokens.lineHeight.normal,
- fontWeight: tokens.fontWeight.normal,
- letterSpacing: tokens.letterSpacing.normal,
-
- color: tokens.color.muted,
-})
-// SB2 in the drawer: a single-section sidebar's header is the title, set like "On this page".
-export const sidebarNodesTitle = style({
- // design-exception: Derive this layout value from the existing responsive CSS variables.
- marginInline: 'calc(var(--spacing) * -2) !custom',
- paddingInline: tokens.spacing['2'],
- paddingBottom: tokens.spacing['2'],
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.compact,
- lineHeight: tokens.lineHeight.compact,
- fontWeight: tokens.fontWeight.medium,
- letterSpacing: tokens.letterSpacing.normal,
-
- color: tokens.color.muted,
-})
-export const sidebarNodesLayout3 = style({
- marginInlineStart: tokens.spacing['2'],
- display: 'flex',
- flexDirection: 'column',
- gap: tokens.spacing['0'],
- borderInlineStartStyle: 'solid',
- borderInlineStartWidth: tokens.borderWidth.hairline,
- borderColor: tokens.color.line,
- paddingInlineStart: tokens.spacing['3'],
-})
-export const sidebarNodesLayout4 = style({
- display: 'flex',
- flexDirection: 'column',
- gap: tokens.spacing['0'],
-})
-export const tempoLogo = style({
- height: '18px',
- width: '80px',
-})
-export const arrowUpRight3 = style({
- width: tokens.spacing['4'],
- height: tokens.spacing['4'],
-})
-export const tempoLogoTextAppearance = instanceStyle(
- (values: { value0: string; value1: string }) => ({
- aspectRatio: '102.461 / 23.2394',
- '--tempo-maskImage': values.value0,
- maskImage: 'var(--tempo-maskImage)',
- maskRepeat: 'no-repeat',
- maskSize: 'contain',
- maskPosition: 'center',
- '--tempo-WebkitMaskImage': values.value1,
- WebkitMaskImage: 'var(--tempo-WebkitMaskImage)',
- WebkitMaskRepeat: 'no-repeat',
- WebkitMaskSize: 'contain',
- WebkitMaskPosition: 'center',
- }),
-)
-export const sidebarLeafAnchorAppearance = instanceStyle((values: { value0: string }) => ({
- '--tempo-paddingLeft': values.value0,
- // design-exception: Preserve the inherited component/framework scope at the point of use.
- paddingInlineStart: 'var(--tempo-paddingLeft) !custom',
-}))
-export const sidebarDisclosureDetailsAppearance = instanceStyle((values: { value0: string }) => ({
- '--tempo-paddingLeft': values.value0,
- // design-exception: Preserve the inherited component/framework scope at the point of use.
- paddingInlineStart: 'var(--tempo-paddingLeft) !custom',
-}))
diff --git a/src/components/DocsHeader.test.ts b/src/components/DocsHeader.test.ts
index 08e68f449..d33b57e61 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 {
+ docsResourceSections,
+ docsSections,
+ docsUtilitySections,
+ getActiveDocsSection,
+} from '../lib/docs-sections'
import { normalizeDocsPath, resolveSidebarItems } from '../lib/docs-sidebar'
const sidebar = {
@@ -41,19 +46,34 @@ describe('resolveSidebarItems', () => {
})
describe('getActiveDocsSection', () => {
- it('presents seven product and developer tabs without availability badges', () => {
+ it('lists the products in the top bar, then Resources as its only menu', () => {
expect(docsSections.map(({ label }) => label)).toEqual([
- 'Get Started',
+ 'Get started',
'Accounts',
'Earn',
'Routes',
'Zones',
'Machine Payments',
- 'Tempo EVM',
+ 'Network',
])
+ expect(docsResourceSections.map(({ label }) => label)).toEqual([
+ 'Guides',
+ 'Partners',
+ 'SDKs & CLI',
+ ])
+ expect(
+ [...docsSections, ...docsUtilitySections]
+ .filter(({ id }) => ['api', 'tools', 'resources', 'ecosystem'].includes(id))
+ .map(({ label }) => label),
+ ).toEqual(['API', 'SDKs & CLI', 'Guides', 'Partners'])
+ expect(getActiveDocsSection('/docs/earn/balances')?.id).toBe('earn')
+ expect(getActiveDocsSection('/docs/cli/request')?.id).toBe('tools')
+ expect(getActiveDocsSection('/docs/partners/wallets')?.id).toBe('ecosystem')
+ expect(getActiveDocsSection('/')).toBeUndefined()
+ expect(getActiveDocsSection('/docs/apiary')).toBeUndefined()
})
- it('opens the guide library from Tempo EVM and the MPP introduction from Machine payments', () => {
+ it('opens Network on the Tempo EVM introduction and Machine Payments on the MPP introduction', () => {
expect(docsSections.find(({ id }) => id === 'developers')?.href).toBe('/docs/development')
expect(docsSections.find(({ id }) => id === 'machine-payments')?.href).toBe('/docs/agents')
})
@@ -62,7 +82,7 @@ describe('getActiveDocsSection', () => {
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/')?.label).toBe('Tempo EVM')
+ expect(getActiveDocsSection('/developers/docs/protocol/')?.label).toBe('Network')
})
it.each([
@@ -125,7 +145,7 @@ describe('getActiveDocsSection', () => {
['/docs/guide/ousd', 'ecosystem'],
['/docs/partners/join', 'ecosystem'],
['/docs/quickstart/integrate-tempo', 'developers'],
- ['/docs/quickstart/connection-details', 'developers'],
+ ['/docs/quickstart/connection-details', 'overview'],
['/docs/guide/issuance/create-a-stablecoin', 'developers'],
['/docs/guide/issuance/manage-stablecoin', 'developers'],
['/docs/guide/issuance/migrate-erc20-to-tip20', 'developers'],
@@ -150,11 +170,13 @@ describe('getActiveDocsSection', () => {
['/docs/changelog', 'developers'],
['/docs/api/indexer-api', 'api'],
['/docs/protocol/rpc/eth_getBalance', 'developers'],
- ['/docs/sdk/typescript', 'overview'],
- ['/docs/cli/request', 'overview'],
- ['/docs/wallet/recipes', 'overview'],
- ['/docs/tools', 'overview'],
+ ['/docs/sdk/typescript', 'tools'],
+ ['/docs/cli/request', 'tools'],
+ ['/docs/wallet/recipes', '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..1ea12dffa 100644
--- a/src/components/DocsHeader.tsx
+++ b/src/components/DocsHeader.tsx
@@ -1,66 +1,38 @@
'use client'
-import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react'
+import { Button, Kbd, Menu } from '@tempoxyz/ds'
+import { useEffect, useLayoutEffect, useState } from 'react'
import { useRouter, Link as WakuLink } from 'waku'
-import { cx as composeStyles } from 'zyzz'
import ChevronDownIcon from '~icons/lucide/chevron-down'
-import SquareTerminalIcon from '~icons/lucide/square-terminal'
-import { tempoAgentSetupCommands } from '../lib/ai-install-commands'
-import { normalizeDocsPath, type SidebarNode } from '../lib/docs-sidebar'
-
-export { normalizeDocsPath, resolveSidebarItems } from '../lib/docs-sidebar'
-
import { DOCS_SEARCH_PARAM } from '../lib/docs-search'
-import { publicAssetPath } from '../lib/public-asset-path'
-import { navActiveSquare } from '../styles/surfaces.styles'
-import { AmpLogo, ClaudeLogo, CodexLogo } from './AgentLogos'
-import { AgentSetupCommand } from './AgentSetupCommand'
-import { CopyIconSwap } from './CopyIconSwap'
-import * as ui from './DocsHeader.recipes'
+import { docsResourceSections, docsSections, getActiveDocsSection } from '../lib/docs-sections'
+import { normalizeDocsPath } from '../lib/docs-sidebar'
+import { DocsLockup } from './DocsLockup'
+import DocsMenuDrawer from './DocsMenuDrawer'
import {
- docsHeaderAccountEmail,
docsHeaderActions,
- docsHeaderAgentMenu,
- docsHeaderAgentPanel,
- docsHeaderAgentTrigger,
- docsHeaderBrand,
- docsHeaderIconButton,
- docsHeaderLoginPrimary,
+ docsHeaderGetKey,
+ docsHeaderKbd,
+ docsHeaderLeft,
docsHeaderLogo,
- docsHeaderMenuButton,
- docsHeaderMobileActions,
- docsHeaderMobileAgents,
- docsHeaderMobileBody,
- docsHeaderMobileDialog,
- docsHeaderMobileLabel,
- docsHeaderMobileResources,
- docsHeaderMobileSearch,
- docsHeaderMobileSections,
- docsHeaderMobileTheme,
- docsHeaderMobileTop,
- docsHeaderMobileUtilityLink,
- docsHeaderMobileWebsite,
docsHeaderNav,
+ docsHeaderSearchField,
+ docsHeaderSearchLabel,
+ docsHeaderWordmark,
+ docsProductChevron,
+ docsProductLink,
+ docsProductNav,
docsSiteHeader,
} from './DocsNavigation.styles'
-import {
- DocsApiDropdown,
- DocsResourceLinks,
- docsSections,
- docsUtilitySections,
- getActiveDocsSection,
-} from './DocsSectionNav'
import { HeadingAnchorFeedback } from './HeadingAnchorFeedback'
-import { iconButton } from './IconButton.recipes'
+import { RouterLink } from './RouterLink'
import { SearchDialogMotion } from './SearchDialogMotion'
import { SmoothCorners } from './SmoothCorners'
-const DOCS_BASE_PATH = '/docs'
-const TEMPO_AI_GUIDE_URL = `${DOCS_BASE_PATH}/guide/using-tempo-with-ai`
-const TEMPO_PLUGIN_URL = `${TEMPO_AI_GUIDE_URL}#install-tempo-plugins`
+export { normalizeDocsPath, resolveSidebarItems } from '../lib/docs-sidebar'
/** Tempo Console. This host forwards to developers.tempo.xyz/console once the console moves there. */
-const CONSOLE_URL = 'https://console.tempo.xyz'
+export const CONSOLE_URL = 'https://console.tempo.xyz'
/** Console session lookup, proxied by this host; its cookie is scoped to `/console` so docs pages never see it. */
const CONSOLE_ME_PATH = '/console/api/v1/me'
@@ -83,10 +55,6 @@ function useConsoleEmail() {
return email
}
-function isExternal(href: string) {
- return !href.startsWith('/') && !href.startsWith('#')
-}
-
export function usePathname() {
const { path } = useRouter()
const [pathname, setPathname] = useState(() => normalizeDocsPath(path ?? '/'))
@@ -109,108 +77,11 @@ export function usePathname() {
return pathname
}
-function Anchor({ href, children, ...props }: React.AnchorHTMLAttributes) {
- if (!href) return {children}
- if (isExternal(href)) {
- return (
-
- {children}
-
- )
- }
- return (
-
- {children}
-
- )
-}
-
-function ArrowUpRight({ className }: { className?: string }) {
- return (
-
-
-
- )
-}
-
-function TempoLogo({ className }: { className?: string }) {
- return (
-
- )
-}
-
-function Glyph({ children }: { children: ReactNode }) {
- return (
-
- {children}
-
- )
-}
-
-function McpIcon() {
- return (
-
-
-
-
-
- )
-}
-
-function ActiveSquare({ activeKey }: { activeKey: string }) {
- return (
-
- {[0, 4, 8].flatMap((y) =>
- [0, 4, 8].map((x) => (
-
- )),
- )}
-
- )
-}
-
-function MenuIcon() {
- return (
-
-
-
- )
-}
-
-function SearchIcon({ className }: { className?: string }) {
+export function SearchIcon() {
return (
@@ -250,7 +120,7 @@ function dispatchDocsSearchShortcut() {
return !document.dispatchEvent(event)
}
-function openDocsSearch() {
+export function openDocsSearch() {
if (!dispatchDocsSearchShortcut() && import.meta.env.DEV) {
console.warn(
'Vocs search did not handle Cmd/Ctrl+K. Verify the hidden Vocs top-nav search is still mounted (showTopNav/showSearch).',
@@ -273,554 +143,72 @@ function openDocsSearchWhenReady(attempt = 0) {
requestAnimationFrame(() => openDocsSearchWhenReady(attempt + 1))
}
-function CloseIcon() {
- return (
-
-
-
- )
-}
-
-function Chevron({ open }: { open: boolean }) {
- return (
-
-
-
- )
-}
+/** The products as plain links, then Resources, the only menu in the bar. */
+function DocsProductNav({ pathname }: { pathname: string }) {
+ const active = getActiveDocsSection(pathname)
+ const inResources = docsResourceSections.some((section) => section.id === active?.id)
-function CopyIcon() {
return (
-
-
-
-
- )
-}
-
-function CheckIcon() {
- return (
-
-
-
- )
-}
-
-// One install path per agent: the plugin bundles the MCP server and docs skill where
-// supported; Skills and MCP expose the client-independent setup options.
-const agentCommands = [
- {
- label: 'Codex',
- logo: ,
- command: tempoAgentSetupCommands.codex,
- },
- {
- label: 'Claude Code',
- logo: ,
- command: tempoAgentSetupCommands.claude,
- },
- {
- label: 'Amp',
- logo: ,
- command: tempoAgentSetupCommands.amp,
- },
- {
- label: 'Skills',
- logo: null,
- command: tempoAgentSetupCommands.skills,
- },
- {
- label: 'MCP',
- logo: null,
- command: tempoAgentSetupCommands.mcp,
- },
-]
-
-function CommandTabs({
- commands,
- activeIndex,
- onSelect,
-}: {
- commands: { label: string; logo: ReactNode }[]
- activeIndex: number
- onSelect: (index: number) => void
-}) {
- return (
-
- {commands.map((item, index) => {
- const active = index === activeIndex
- return (
- onSelect(index)}
- {...composeStyles(
- ui.commandTabsButton(),
- !!active && ui.commandTabsButton2(),
- !active && ui.commandTabsButton3(),
- )}
- >
- {item.logo}
- {item.label}
-
- )
- })}
-
- )
-}
-
-function CommandSnippet({
- command,
- copyLabel,
- copied,
- onCopy,
- children,
-}: {
- command: string
- copyLabel: string
- copied: boolean
- onCopy: (command: string) => void
- children?: ReactNode
-}) {
- return (
- onCopy(command)}
- aria-label={copyLabel}
- {...ui.commandSnippetButton({ className: 'group/copy' })}
- >
-
-
- $
-
-
- {children ?? }
-
-
-
- } checkIcon={ } />
-
-
- )
-}
-
-function AgentCommandSection(props: {
- href: string
- label: string
- desc: string
- icon: ReactNode
- onClick?: () => void
- children?: ReactNode
-}) {
- const { href, label, desc, icon, onClick, children } = props
- const external = isExternal(href)
-
- return (
-
-
-
{icon}
-
- {external ? : null}
- {label}
- {desc}
-
-
- {children ?
{children}
: null}
-
- )
-}
-
-function AgentsPanel({
- variant = 'desktop',
- onNavigate,
-}: {
- variant?: 'desktop' | 'mobile'
- onNavigate?: () => void
-}) {
- const desktop = variant === 'desktop'
- const [activeCommandIndex, setActiveCommandIndex] = useState(0)
- const [copied, setCopied] = useState(false)
- const activeCommand = agentCommands[activeCommandIndex]
-
- const copyCommand = async (command: string) => {
- try {
- await navigator.clipboard.writeText(command)
- setCopied(true)
- setTimeout(() => setCopied(false), 1500)
- } catch {}
- }
-
- return (
-
- {desktop ?
Connect your coding agent
: null}
-
-
}
- onClick={onNavigate}
- >
-
{
- setActiveCommandIndex(index)
- setCopied(false)
- }}
- />
- {activeCommand.label === 'MCP' ? (
-
- Add this URL as an HTTP MCP server in your agent’s settings.
-
- ) : null}
-
-
-
-
- )
-}
-
-function sidebarLinkIsActive(pathname: string, link: string, activeAnchor: string | null) {
- if (isExternal(link)) return false
- const [target, fragment] = link.split('#')
- const targetPath = normalizeDocsPath(target?.split('?')[0] || pathname).replace(/\/+$/, '')
- if (normalizeDocsPath(pathname).replace(/\/+$/, '') !== targetPath) return false
- if (!fragment) return activeAnchor === null
-
- try {
- return decodeURIComponent(fragment) === activeAnchor
- } catch {
- return fragment === activeAnchor
- }
-}
-
-function nodeContainsActive(
- node: SidebarNode,
- pathname: string,
- activeAnchor: string | null,
-): boolean {
- if (node.link && sidebarLinkIsActive(pathname, node.link, activeAnchor)) return true
- return Boolean(node.items?.some((child) => nodeContainsActive(child, pathname, activeAnchor)))
-}
-
-function SidebarLeaf({
- node,
- pathname,
- activeAnchor,
- depth,
- onNavigate,
-}: {
- node: SidebarNode
- pathname: string
- activeAnchor: string | null
- depth: number
- onNavigate: () => void
-}) {
- const active = node.link ? sidebarLinkIsActive(pathname, node.link, activeAnchor) : false
- const external = node.link ? isExternal(node.link) : false
- return (
- 1 ? `${(depth - 1) * 12 + 8}px` : '8px',
- className: ` ${ui.anchor2().className} ${
- active ? ui.anchor3().className : ui.anchor4().className
- }`,
- })}
- >
- {active ? : null}
- {node.text}
- {external ? : null}
-
- )
-}
-
-function SidebarDisclosure({
- node,
- pathname,
- activeAnchor,
- depth,
- onNavigate,
-}: {
- node: SidebarNode
- pathname: string
- activeAnchor: string | null
- depth: number
- onNavigate: () => void
-}) {
- const containsActivePage = nodeContainsActive(node, pathname, activeAnchor)
- const [open, setOpen] = useState(() => !node.collapsed || containsActivePage)
-
- // Match the desktop sidebar: reveal newly selected pages while preserving
- // groups the reader has opened themselves.
- useEffect(() => {
- if (containsActivePage) setOpen(true)
- }, [containsActivePage])
-
- return (
- setOpen(event.currentTarget.open)}
- {...ui.sidebarDisclosureDetailsAppearance({
- value0: depth > 1 ? `${(depth - 1) * 12}px` : '0px',
- className: ui.sidebarDisclosureDetails({ className: 'group/sb' }).className,
- })}
- >
-
- {node.text}
-
-
-
-
-
-
- )
-}
-
-export function SidebarNodes({
- nodes,
- pathname,
- activeAnchor,
- depth,
- onNavigate,
-}: {
- nodes: SidebarNode[]
- pathname: string
- activeAnchor: string | null
- depth: number
- onNavigate: () => void
-}) {
- return (
- <>
- {nodes.map((node, i) => {
- const key = `${node.text ?? node.link ?? 'node'}-${i}`
- const hasChildren = Array.isArray(node.items) && node.items.length > 0
-
- // Leaf link.
- if (!hasChildren) {
- return (
-
- )
- }
-
- // Non-collapsible section (e.g. "Build on Tempo"): a heading + children.
- if (node.collapsed === undefined) {
- return (
-
+
+ {docsSections.map((section) => (
+
+
-
- {node.text}
-
- 0
- ? ui.sidebarNodesLayout3().className
- : ui.sidebarNodesLayout4().className
- }
- >
-
-
-
- )
- }
-
- return (
-
- )
- })}
- >
+ {section.label}
+
+
+ ))}
+
+
+
+ Resources
+
+
+
+ {docsResourceSections.map((section) => (
+ }
+ >
+ {section.label}
+
+ ))}
+
+
+
+
+
)
}
export default function DocsHeader() {
const pathname = usePathname()
- const activeSection = getActiveDocsSection(pathname)
- const [open, setOpen] = useState(false)
const [mounted, setMounted] = useState(false)
- const [agentsOpen, setAgentsOpen] = useState(false)
const email = useConsoleEmail()
- // Keep the Agent setup panel rendered through its exit fade (G5).
- const [agentsRendered, setAgentsRendered] = useState(false)
- useEffect(() => {
- if (agentsOpen) {
- setAgentsRendered(true)
- return
- }
- const timer = window.setTimeout(() => setAgentsRendered(false), 150)
- return () => window.clearTimeout(timer)
- }, [agentsOpen])
- const agentMenuRef = useRef(null)
- const agentTriggerRef = useRef(null)
- const mobileDialogRef = useRef(null)
- const mobileCloseRef = useRef(null)
-
- const close = () => {
- setOpen(false)
- setAgentsOpen(false)
- }
-
- const openSearch = () => {
- mobileDialogRef.current?.close()
- close()
- requestAnimationFrame(openDocsSearch)
- }
+ // Get API key opens Console, which signs in; once signed in, the button names Console itself.
+ const consoleLabel = email ? 'Console' : 'Get API key'
+ const consoleTitle = email ? `Signed in as ${email}` : undefined
useEffect(() => {
setMounted(true)
}, [])
- // Close menus after navigation, including browser back/forward.
- useEffect(() => {
- setOpen(false)
- setAgentsOpen(false)
- }, [pathname])
-
- useEffect(() => {
- const dialog = mobileDialogRef.current
- if (!dialog) return
- if (!open) {
- dialog.close()
- return
- }
- dialog.showModal()
- mobileCloseRef.current?.focus()
- const previousOverflow = document.body.style.overflow
- document.body.style.overflow = 'hidden'
- // A resized desktop viewport must not retain an invisible modal.
- const desktop = window.matchMedia('(min-width: 1080px)')
- const onResize = () => {
- if (desktop.matches) setOpen(false)
- }
- desktop.addEventListener('change', onResize)
- return () => {
- document.body.style.overflow = previousOverflow
- desktop.removeEventListener('change', onResize)
- dialog.close()
- }
- }, [open])
-
- useEffect(() => {
- if (!agentsOpen) return
- const closeWhenOutside = (event: Event) => {
- if (!agentMenuRef.current?.contains(event.target as Node)) setAgentsOpen(false)
- }
- const onKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'Escape') {
- setAgentsOpen(false)
- agentTriggerRef.current?.focus()
- }
- }
- const desktop = window.matchMedia('(min-width: 1080px)')
- const onResize = () => {
- if (!desktop.matches) setAgentsOpen(false)
- }
- document.addEventListener('pointerdown', closeWhenOutside)
- document.addEventListener('focusin', closeWhenOutside)
- document.addEventListener('keydown', onKeyDown)
- desktop.addEventListener('change', onResize)
- return () => {
- document.removeEventListener('pointerdown', closeWhenOutside)
- document.removeEventListener('focusin', closeWhenOutside)
- document.removeEventListener('keydown', onKeyDown)
- desktop.removeEventListener('change', onResize)
- }
- }, [agentsOpen])
-
// Open search when arriving from the website with ?search=1.
useEffect(() => {
const params = new URLSearchParams(window.location.search)
@@ -835,248 +223,54 @@ export default function DocsHeader() {
openDocsSearchWhenReady()
}, [])
- const brand = (
-
-
-
-
-
- )
-
return (
)
}
-
-function ThemeSelect() {
- const [theme, setTheme] = useState('system')
- useEffect(() => {
- const sync = () => setTheme(localStorage.getItem('vocs-theme') ?? 'system')
- sync()
- const observer = new MutationObserver(sync)
- observer.observe(document.documentElement, {
- attributes: true,
- attributeFilter: ['data-vocs-theme'],
- })
- return () => observer.disconnect()
- }, [])
-
- return (
-
- Appearance
- {
- const selected = event.target.value
- const label = selected.charAt(0).toUpperCase() + selected.slice(1)
- const control = document.querySelector(
- `[data-v-sidebar-footer-content] [role="radio"][aria-label="${label} theme"]`,
- )
- if (control) control.click()
- else {
- localStorage.setItem('vocs-theme', selected)
- const resolved =
- selected === 'system'
- ? window.matchMedia('(prefers-color-scheme: dark)').matches
- ? 'dark'
- : 'light'
- : selected
- document.documentElement.dataset.vocsTheme = resolved
- document.documentElement.style.colorScheme = resolved
- }
- setTheme(selected)
- }}
- >
- Light
- Dark
- System
-
-
- )
-}
diff --git a/src/components/DocsHome.styles.ts b/src/components/DocsHome.styles.ts
index ce2af14c4..9d57bc5de 100644
--- a/src/components/DocsHome.styles.ts
+++ b/src/components/DocsHome.styles.ts
@@ -2,75 +2,74 @@ import { inherited } from '../styles/inherited'
import { style } from '../styles/scoped'
import { vars as tokens } from '../styles/theme'
+// Home: the hero (eyebrow, title, lead, one primary button), then Start with a guide,
+// Products and Reference & tools. Every card is the page background with a 1px border,
+// radius 12 and padding 20, 16 apart: title 16 medium, description 14/20 secondary.
export const tempoDocsHome = style({
- '--home-muted': 'var(--vocs-text-color-secondary)',
- '--home-ink': 'var(--vocs-text-color-primary)',
- '--home-panel': tokens.color.panel,
-
- color: inherited.color.homeInk,
-
- fontFamily: inherited.fontFamily.tempoFontBodyVarVocsFontFamily,
+ color: tokens.color.foreground,
+ fontFamily: tokens.fontFamily.ui,
selectors: {
'& span[id]': {
display: 'block',
scrollMarginTop: 'calc(var(--vocs-spacing-topNav) + 32px)',
},
- // Every Home section sits one section token (80px, 64px under 768px) below the last.
- '& .tempo-docs-home-heading': {
- paddingBottom: tokens.spacing.section,
+ '& .tempo-docs-home-eyebrow': {
+ marginTop: tokens.spacing['0'],
+ marginInlineEnd: tokens.spacing['0'],
+ marginBottom: tokens.spacing['3'],
+ marginInlineStart: tokens.spacing['0'],
+
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.control,
},
+ // Home's display title: 48 from Tablet up, 32 on Mobile; H1 → lead 12.
'& h1[data-v]': {
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- margin: '0 0 18px !custom',
+ marginTop: tokens.spacing['0'],
+ marginInlineEnd: tokens.spacing['0'],
+ marginBottom: tokens.spacing['3'],
+ marginInlineStart: tokens.spacing['0'],
padding: 0,
border: 0,
- fontFamily: inherited.fontFamily.tempoFontDisplayVarVocsFontFamily,
+ fontFamily: tokens.fontFamily.book,
fontSize: tokens.fontSize.displayLarge,
- fontWeight: tokens.fontWeight.medium,
+ fontWeight: tokens.fontWeight.normal,
letterSpacing: tokens.letterSpacing.heading,
lineHeight: tokens.lineHeight.display,
+ textWrap: 'balance',
},
'& .tempo-docs-home-heading p[data-v]': {
margin: 0,
- color: inherited.color.homeMuted,
- fontSize: tokens.fontSize.body,
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.lead,
lineHeight: tokens.lineHeight.relaxed,
+ textWrap: 'pretty',
+ },
+ // Lead → the hero's one button 32.
+ '& .tempo-docs-home-actions': {
+ display: 'flex',
+ marginTop: tokens.spacing['8'],
},
- // Build with your agent, then Start with a guide, stacked a section apart.
- '& .tempo-docs-home-start': {
- display: 'grid',
- gridTemplateColumns: 'minmax(0, 1fr)',
- rowGap: tokens.spacing.section,
+ // Sections: H2 48 above; H2 → card grid 24 (the block rhythm).
+ '& :is(.tempo-docs-home-guides, .tempo-docs-home-products, .tempo-docs-home-reference)': {
+ marginTop: tokens.spacing['12'],
},
'& h2[data-v]': {
margin: 0,
padding: 0,
border: 0,
- fontSize: tokens.fontSize.title,
- fontWeight: tokens.fontWeight.medium,
- letterSpacing: tokens.letterSpacing.heading,
+ fontFamily: tokens.fontFamily.book,
+ fontSize: tokens.fontSize.title,
+ fontWeight: tokens.fontWeight.normal,
+ letterSpacing: tokens.letterSpacing.tight,
lineHeight: tokens.lineHeight.compact,
},
- // Start with a guide: a 3-up of hairline-topped columns, each a description
- // and a secondary button named for the guide. Buttons align at the column end.
- '& .tempo-docs-home-guide-grid': {
- display: 'grid',
- gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
- gap: tokens.spacing['5'],
- marginTop: tokens.spacing['7'],
- },
- '& .tempo-docs-home-guide': {
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'flex-start',
- gap: tokens.spacing['4'],
- paddingTop: tokens.spacing['4'],
- borderTopWidth: tokens.borderWidth.hairline,
- borderTopStyle: 'solid',
- borderTopColor: tokens.color.hairline,
+ '& .heading-anchor': {
+ display: 'none',
},
// Start with a guide: the heading row carries Find your starting point at the inline end.
'& .tempo-docs-home-section-head': {
@@ -81,59 +80,112 @@ export const tempoDocsHome = style({
columnGap: tokens.spacing['4'],
rowGap: tokens.spacing['2'],
},
- // Each guide's name heads its column; the button below reads "Get started".
- '& .tempo-docs-home-guide h3[data-v]': {
- margin: 0,
- padding: 0,
-
- color: inherited.color.homeInk,
- fontSize: tokens.fontSize.lead,
- fontWeight: tokens.fontWeight.medium,
- letterSpacing: tokens.letterSpacing.tight,
- lineHeight: tokens.lineHeight.snug,
- },
- '& .tempo-docs-home-guide .heading-anchor': {
- display: 'none',
- },
- '& .tempo-docs-home-guide p[data-v]': {
- margin: 0,
-
- color: inherited.color.homeMuted,
- fontSize: tokens.fontSize.sm,
- lineHeight: tokens.lineHeight.relaxed,
- textWrap: 'pretty',
- },
- '& .tempo-docs-home-guide-button': {
- // design-exception: Pushes the button to the column end so the 3-up buttons align.
- marginTop: 'auto !custom',
- },
- // Same style as All setup options.
'& .tempo-docs-home-guides-more': {
- color: inherited.color.homeInk,
- fontSize: tokens.fontSize.compact,
- lineHeight: tokens.lineHeight.relaxed,
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
textDecoration: 'none',
+ transitionProperty: 'color, background-color, border-color, opacity, transform',
+ transitionDuration: 'var(--tempo-exit)',
+ transitionTimingFunction: 'var(--tempo-ease)',
},
- '& .tempo-docs-home-products': {
- marginTop: tokens.spacing.section,
+ '& :is(.tempo-docs-home-guide-grid, .tempo-docs-home-product-grid, .tempo-docs-home-quick-links)':
+ {
+ display: 'grid',
+ gap: tokens.spacing['4'],
+ marginTop: tokens.spacing['6'],
+ },
+ '& .tempo-docs-home-guide-grid': {
+ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
},
'& .tempo-docs-home-product-grid': {
- display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
- gap: tokens.spacing['5'],
- marginTop: tokens.spacing['7'],
},
- '& .tempo-docs-home-product-group': {
+ '& .tempo-docs-home-quick-links': {
+ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
+ },
+ // The one card family.
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-product-group, .tempo-docs-home-quick-link)': {
minWidth: 0,
- // design-exception: Preserve the inherited component/framework scope at the point of use.
- padding: 'var(--tempo-card-padding) !custom',
- // design-exception: Preserve the inherited component/framework scope at the point of use.
- borderRadius: 'var(--tempo-card-radius) !custom',
- '--corner-radius': 'var(--tempo-card-radius)',
- backgroundColor: inherited.color.homePanel,
+ padding: tokens.spacing['5'],
+ borderWidth: tokens.borderWidth.hairline,
+ borderStyle: 'solid',
+ borderColor: tokens.color.hairline,
+ borderRadius: tokens.radius.lg,
+ '--corner-radius': tokens.radius.lg,
},
- // The title sits at the start and the outline badge at the inline end. The row
- // keeps the badge's height so tiles with and without a badge align.
+ // Card titles 16 medium primary.
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-product-group) h3[data-v], & .tempo-docs-home-quick-link > a':
+ {
+ margin: 0,
+ padding: 0,
+
+ color: tokens.color.foreground,
+ fontFamily: tokens.fontFamily.ui,
+ fontSize: tokens.fontSize.body,
+ fontWeight: tokens.fontWeight.medium,
+ letterSpacing: tokens.letterSpacing.normal,
+ lineHeight: tokens.lineHeight.body,
+ textDecoration: 'none',
+ },
+ // Card descriptions 14/20 secondary, 4 below the title.
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-product-group, .tempo-docs-home-quick-link) p[data-v]':
+ {
+ margin: 0,
+
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ textWrap: 'pretty',
+ },
+ // A guide or quick-link card has one destination, so its link covers the card. The card
+ // takes the hover fill and the focus ring.
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-quick-link)': {
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: tokens.spacing['1'],
+ transitionProperty: 'color, background-color, border-color, opacity, transform',
+ transitionDuration: 'var(--tempo-exit)',
+ transitionTimingFunction: 'var(--tempo-ease)',
+ },
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-quick-link) > a::after': {
+ content: '""',
+ position: 'absolute',
+ insetBlock: tokens.spacing['0'],
+ insetInline: tokens.spacing['0'],
+ },
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-quick-link) > a:focus-visible': {
+ outlineStyle: 'none',
+ },
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-quick-link):has(> a:focus-visible)': {
+ outlineWidth: tokens.borderWidth.emphasis,
+ outlineStyle: 'solid',
+ outlineColor: tokens.color.foreground,
+ outlineOffset: '4px',
+ },
+ // Guides: the action sits at the card end, so the three actions align.
+ // Chevron links stay inline-block: a flex link would drop the space before the last word.
+ '& .tempo-docs-home-guide-action': {
+ display: 'inline-block',
+ minHeight: '24px',
+ // design-exception: Pushes the action to the card end so the three actions align.
+ marginTop: 'auto !custom',
+ paddingTop: tokens.spacing['3'],
+
+ color: tokens.color.foreground,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.control,
+ textDecoration: 'none',
+ },
+ // Reference & tools: a 32 icon tile, then the title and description.
+ '& .tempo-docs-home-quick-link > .tempo-docs-home-icon-tile': {
+ marginBottom: tokens.spacing['3'],
+ },
+ // Products: the icon and linked title at the start, the outline badge at the inline end.
+ // The row keeps the badge's height so cards with and without a badge align.
'& .tempo-docs-home-product-header': {
display: 'flex',
alignItems: 'center',
@@ -142,35 +194,19 @@ export const tempoDocsHome = style({
minHeight: '28px',
},
'& .tempo-docs-home-product-group h3[data-v]': {
- margin: 0,
- padding: 0,
- fontSize: tokens.fontSize.lead,
- fontWeight: tokens.fontWeight.medium,
-
- letterSpacing: tokens.letterSpacing.tight,
- lineHeight: tokens.lineHeight.snug,
+ display: 'flex',
+ alignItems: 'center',
},
'& .tempo-docs-home-product-group h3[data-v] > a:not(.heading-anchor)': {
- color: inherited.color.homeInk,
+ color: tokens.color.foreground,
fontWeight: 'inherit !custom',
textDecoration: 'none',
},
- // Tile titles are chevron links; the heading keeps its level but not the copy-link anchor.
- '& .tempo-docs-home-product-group .heading-anchor': {
- display: 'none',
- },
+ // Two lines of description keep the link lists level across a row.
'& .tempo-docs-home-product-group p[data-v]': {
- minHeight: '3.2em',
-
- marginTop: tokens.spacing['3'],
- marginInlineEnd: tokens.spacing['0'],
- marginBottom: tokens.spacing['5'],
- marginInlineStart: tokens.spacing['0'],
-
- color: inherited.color.homeMuted,
- fontSize: tokens.fontSize.sm,
- lineHeight: tokens.lineHeight.relaxed,
- textWrap: 'balance',
+ minHeight: '40px',
+ marginTop: tokens.spacing['1'],
+ marginBottom: tokens.spacing['4'],
},
'& ul[data-v]': {
margin: 0,
@@ -180,127 +216,87 @@ export const tempoDocsHome = style({
'& li[data-v]': {
margin: 0,
padding: 0,
- fontSize: tokens.fontSize.bodySmall,
- lineHeight: tokens.lineHeight.relaxed,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ },
+ '& li[data-v]::marker': {
+ content: 'none',
},
'& li[data-v] + li[data-v]': {
- marginTop: tokens.spacing['2_5'],
+ marginTop: tokens.spacing['2'],
},
- // Product sub-links are chevron links: no underline at rest or on hover.
'& .tempo-docs-home-product-group li[data-v] a': {
- color: inherited.color.homeInk,
+ color: tokens.color.foreground,
fontWeight: tokens.fontWeight.normal,
textDecoration: 'none',
},
- '& p[data-v] a[data-v]': {
- color: tokens.color.accent,
- fontWeight: tokens.fontWeight.normal,
- textDecoration: 'underline',
-
- textDecorationColor: inherited.color.colorMixInSrgbAccentBlue40Transparent,
- textDecorationThickness: '1px',
- textUnderlineOffset: '3px',
- },
- '& .tempo-docs-home-reference': {
- marginTop: tokens.spacing.section,
- },
- '& .tempo-docs-home-reference-grid': {
- display: 'grid',
- gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
- gap: tokens.spacing['12'],
- marginTop: tokens.spacing['7'],
- },
- '& .tempo-docs-home-reference-grid h3[data-v]': {
- marginTop: tokens.spacing['0'],
- marginInlineEnd: tokens.spacing['0'],
- marginBottom: tokens.spacing['4'],
- marginInlineStart: tokens.spacing['0'],
- padding: 0,
-
- color: inherited.color.homeMuted,
- fontSize: tokens.fontSize.sm,
- fontWeight: tokens.fontWeight.normal,
- letterSpacing: tokens.letterSpacing.normal,
- },
- '& .tempo-docs-home-reference-grid > div:first-child ul[data-v]': {
- display: 'grid',
- gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- gap: '10px 20px !custom',
- },
- '& .tempo-docs-home-reference-grid > div:first-child li[data-v]': {
- margin: 0,
- },
- // "All APIs & SDKs" leads the group on its own row, above the SDK pairs.
- '& .tempo-docs-home-reference-grid > div:first-child li[data-v]:first-child': {
- gridColumn: '1 / -1',
- },
// Standalone links get at least a 24px hit area.
- '& .tempo-docs-home-guides-more, & .tempo-agent-start-footer > a, & .tempo-agent-start-prerequisite, & .tempo-docs-home-product-group :is(h3[data-v], li[data-v]) > a:not(.heading-anchor), & .tempo-docs-home-reference-grid li a':
+ '& .tempo-docs-home-guides-more, & .tempo-docs-home-product-group :is(h3[data-v], li[data-v]) > a:not(.heading-anchor)':
{
display: 'inline-block',
minHeight: '24px',
},
- // SegmentedControl items keep their inset ring; an outset one is clipped by the scroller.
- '& :is(a, button):not([role="radio"]):focus-visible': {
+ '& :is(a, button):focus-visible': {
outlineWidth: tokens.borderWidth.emphasis,
outlineStyle: 'solid',
- outlineColor: inherited.color.homeInk,
+ outlineColor: tokens.color.foreground,
outlineOffset: '4px',
},
- // The docs-wide focus rule (outset 3px) outranks the SegmentedControl recipe; restore its inset ring.
- '& [role="radio"]:focus-visible': {
- outlineWidth: tokens.borderWidth.emphasis,
- outlineStyle: 'solid',
- outlineColor: 'currentColor !custom',
- outlineOffset: '-2px',
+ },
+ '@media (hover: hover)': {
+ selectors: {
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-quick-link):has(> a:hover)': {
+ backgroundColor: tokens.color.containerSubtle,
+ transitionDuration: 'var(--tempo-enter)',
+ },
+ '& .tempo-docs-home-guides-more:hover': {
+ color: tokens.color.foreground,
+ transitionDuration: 'var(--tempo-enter)',
+ },
+ '& .tempo-docs-home-product-group :is(h3[data-v] > a:not(.heading-anchor), li[data-v] a):hover':
+ {
+ textDecoration: 'underline',
+ textUnderlineOffset: '4px',
+ },
},
},
- '@media (width < 1100px)': {
+ '@media (prefers-reduced-motion: reduce)': {
selectors: {
- '& .tempo-docs-home-product-grid': {
- gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
+ '& :is(.tempo-docs-home-guide, .tempo-docs-home-quick-link, .tempo-docs-home-guides-more)': {
+ transitionProperty: 'none',
},
},
},
- '@media (width < 800px)': {
+ // Below Desktop: products and quick links in two columns, guides stacked.
+ '@media (width < 1080px)': {
selectors: {
- '& .tempo-docs-home-reference-grid': {
+ '& :is(.tempo-docs-home-product-grid, .tempo-docs-home-quick-links)': {
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
- gap: tokens.spacing['8'],
},
},
},
- '@media (width < 768px)': {
+ '@media (width < 900px)': {
selectors: {
'& .tempo-docs-home-guide-grid': {
gridTemplateColumns: 'minmax(0, 1fr)',
- gap: tokens.spacing['7'],
},
},
},
- '@media (width < 700px)': {
+ '@media (width < 768px)': {
selectors: {
- '& .tempo-docs-home-product-grid': {
- gridTemplateColumns: 'minmax(0, 1fr)',
- gap: tokens.spacing['4'],
- },
- '& .tempo-docs-home-product-group p[data-v]': {
- minHeight: 0,
+ '& h1[data-v]': {
+ fontSize: tokens.fontSize.displaySmall,
},
},
},
- '@media (width < 520px)': {
+ // Mobile: one column of cards.
+ '@media (width < 640px)': {
selectors: {
- '& h1[data-v]': {
- fontSize: tokens.fontSize.display,
- },
- '& .tempo-docs-home-heading p[data-v]': {
- fontSize: tokens.fontSize.bodySmall,
- },
- '& .tempo-docs-home-reference-grid': {
+ '& :is(.tempo-docs-home-product-grid, .tempo-docs-home-quick-links)': {
gridTemplateColumns: 'minmax(0, 1fr)',
- gap: tokens.spacing['8'],
+ },
+ '& .tempo-docs-home-product-group p[data-v]': {
+ minHeight: 0,
},
},
},
@@ -308,15 +304,31 @@ export const tempoDocsHome = style({
export const tempoDocsHomeProductIcon = style({
display: 'inline-block',
+ flexShrink: 0,
width: '20px',
height: '20px',
- marginInlineEnd: tokens.spacing['2_5'],
- verticalAlign: '-4px',
+ marginInlineEnd: tokens.spacing['2'],
+
+ color: tokens.color.foreground,
+})
- color: inherited.color.homeInk,
+// A 32 icon tile (radius 8, container fill) holding a 16 icon.
+export const tempoDocsHomeIconTile = style({
+ display: 'inline-flex',
+ flexShrink: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '32px',
+ height: '32px',
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ backgroundColor: tokens.color.container,
+
+ color: tokens.color.foreground,
})
-// Build with your agent sits on the page (no tile); its terminal takes the panel fill.
+// DocsHomeAgent (Build with your agent). Home no longer renders it; it reads the --home-* colours
+// from its host.
export const tempoAgentStart = style({
minWidth: 0,
selectors: {
@@ -334,19 +346,15 @@ export const tempoAgentStart = style({
justifyContent: 'space-between',
gap: tokens.spacing['4'],
},
- // The agent block is an outlined tile, like the setup tiles on Accounts: page
- // fill and a hairline (G4), the card radius and padding.
+ // The agent block is a Home card: the docs card fill, radius and padding.
'& .tempo-agent-start-panel': {
marginTop: tokens.spacing['7'],
- // design-exception: Shares the Vocs card padding variable with the setup tiles.
+ // design-exception: Shares the docs card padding variable with Vocs cards.
padding: 'var(--tempo-card-padding) !custom',
- borderWidth: tokens.borderWidth.hairline,
- borderStyle: 'solid',
- borderColor: tokens.color.hairline,
- // design-exception: Shares the card radius variable with the setup tiles.
+ // design-exception: Shares the docs card radius variable with Vocs cards.
borderRadius: 'var(--tempo-card-radius) !custom',
'--corner-radius': 'var(--tempo-card-radius)',
- backgroundColor: tokens.color.background,
+ backgroundColor: inherited.color.homePanel,
},
// Connect Tempo docs, with the agent's install link at the inline end.
'& .tempo-agent-start-heading': {
@@ -378,12 +386,13 @@ export const tempoAgentStart = style({
width: '14px',
height: '14px',
},
+ // The terminal sits inside the card, so it takes the page fill.
'& .tempo-agent-start-command': {
marginTop: tokens.spacing['3'],
overflow: 'hidden',
borderRadius: tokens.radius.xl,
'--corner-radius': tokens.radius.xl,
- backgroundColor: inherited.color.homePanel,
+ backgroundColor: tokens.color.background,
},
'& .tempo-agent-start-destination': {
display: 'inline-flex',
diff --git a/src/components/DocsHomeLinks.tsx b/src/components/DocsHomeLinks.tsx
index de74b3a2b..70b03065b 100644
--- a/src/components/DocsHomeLinks.tsx
+++ b/src/components/DocsHomeLinks.tsx
@@ -17,16 +17,20 @@ export function HomeColorLink({ href, children }: { href: string; children: Reac
)
}
-/** A secondary (gray) "Get started" button under a guide in the home 3-up. The
- * guide's name is the column heading, so the accessible name carries it too. */
-export function HomeGuideButton({ href, guide }: { href: string; guide: string }) {
+/** A Home action button: the accent fill for the page's primary action, the gray fill
+ * for the secondary one. Its label is the action itself, so it carries no extra name. */
+export function HomeGuideButton({
+ href,
+ guide,
+ variant = 'default',
+}: {
+ href: string
+ guide: string
+ variant?: 'accent' | 'default'
+}) {
return (
-
- Get started
+
+ {guide}
)
}
diff --git a/src/components/DocsHomeProductIcon.tsx b/src/components/DocsHomeProductIcon.tsx
index edeea03af..b3d1f387c 100644
--- a/src/components/DocsHomeProductIcon.tsx
+++ b/src/components/DocsHomeProductIcon.tsx
@@ -1,8 +1,10 @@
import ArrowLeftRightIcon from '~icons/lucide/arrow-left-right'
import BotIcon from '~icons/lucide/bot'
+import BracesIcon from '~icons/lucide/braces'
import CodeXmlIcon from '~icons/lucide/code-xml'
import CoinsIcon from '~icons/lucide/coins'
import CreditCardIcon from '~icons/lucide/credit-card'
+import DropletIcon from '~icons/lucide/droplet'
import FingerprintIcon from '~icons/lucide/fingerprint-pattern'
import KeyRoundIcon from '~icons/lucide/key-round'
import LifeBuoyIcon from '~icons/lucide/life-buoy'
@@ -11,9 +13,10 @@ import NetworkIcon from '~icons/lucide/network'
import RepeatIcon from '~icons/lucide/repeat'
import SendIcon from '~icons/lucide/send'
import ShieldCheckIcon from '~icons/lucide/shield-check'
+import SquareTerminalIcon from '~icons/lucide/square-terminal'
import TrendingUpIcon from '~icons/lucide/trending-up'
import WalletIcon from '~icons/lucide/wallet'
-import { tempoDocsHomeProductIcon } from './DocsHome.styles'
+import { tempoDocsHomeIconTile, tempoDocsHomeProductIcon } from './DocsHome.styles'
export const docsProductIcons = {
accounts: WalletIcon,
@@ -31,10 +34,26 @@ export const docsProductIcons = {
money: CoinsIcon,
card: CreditCardIcon,
automations: RepeatIcon,
+ api: BracesIcon,
+ faucet: DropletIcon,
+ sdk: SquareTerminalIcon,
}
-export function DocsHomeProductIcon({ product }: { product: keyof typeof docsProductIcons }) {
+/** A product icon: 20 beside a card title, or 16 in a 32 icon tile (`tile`). */
+export function DocsHomeProductIcon({
+ product,
+ tile = false,
+}: {
+ product: keyof typeof docsProductIcons
+ tile?: boolean
+}) {
const Icon = docsProductIcons[product]
+ if (tile)
+ return (
+
+
+
+ )
return (
+
+
+
+ )
+}
diff --git a/src/components/DocsMenuDrawer.tsx b/src/components/DocsMenuDrawer.tsx
new file mode 100644
index 000000000..b63213a74
--- /dev/null
+++ b/src/components/DocsMenuDrawer.tsx
@@ -0,0 +1,280 @@
+'use client'
+
+import { Button, Drawer, SidebarMenu } from '@tempoxyz/ds'
+import { type ComponentType, type SVGProps, useEffect, useState } from 'react'
+import { useConfig } from 'vocs'
+import ArrowUpRightIcon from '~icons/lucide/arrow-up-right'
+import BookOpenIcon from '~icons/lucide/book-open'
+import CompassIcon from '~icons/lucide/compass'
+import HandshakeIcon from '~icons/lucide/handshake'
+import MenuIcon from '~icons/lucide/menu'
+import SearchIcon from '~icons/lucide/search'
+import TerminalIcon from '~icons/lucide/terminal'
+import {
+ type DocsSection,
+ docsResourceSections,
+ docsSections,
+ getActiveDocsSection,
+} from '../lib/docs-sections'
+import { normalizeDocsPath, resolveSidebarItems, type SidebarNode } from '../lib/docs-sidebar'
+import { docsProductIcons } from './DocsHomeProductIcon'
+import { DocsLockup } from './DocsLockup'
+import {
+ docsHeaderMenuButton,
+ docsHeaderWordmark,
+ docsMenuDrawerDivider,
+ docsMenuDrawerFooter,
+ docsMenuDrawerPopup,
+ docsMenuDrawerSearch,
+ docsMenuDrawerTheme,
+ docsMenuDrawerTitle,
+} from './DocsNavigation.styles'
+import { RouterLink } from './RouterLink'
+
+type Icon = ComponentType>
+
+const sectionIcons: Partial> = {
+ overview: CompassIcon,
+ accounts: docsProductIcons.accounts,
+ earn: docsProductIcons.earn,
+ routes: docsProductIcons.routes,
+ zones: docsProductIcons.zones,
+ 'machine-payments': docsProductIcons.agents,
+ developers: docsProductIcons.network,
+ resources: BookOpenIcon,
+ ecosystem: HandshakeIcon,
+ tools: TerminalIcon,
+}
+
+function isExternal(href: string) {
+ return !href.startsWith('/') && !href.startsWith('#')
+}
+
+function isCurrentPage(pathname: string, link: string) {
+ const target = normalizeDocsPath(link.split('#')[0]?.split('?')[0] || '/').replace(/\/+$/, '')
+ return normalizeDocsPath(pathname).replace(/\/+$/, '') === (target || '/')
+}
+
+function containsCurrentPage(node: SidebarNode, pathname: string): boolean {
+ if (node.link && !isExternal(node.link) && isCurrentPage(pathname, node.link)) return true
+ return Boolean(node.items?.some((child) => containsCurrentPage(child, pathname)))
+}
+
+function SectionItem({ section, current }: { section: DocsSection; current: boolean }) {
+ const Icon = sectionIcons[section.id]
+ return (
+ }>
+ {Icon ? : null}
+ {section.label}
+
+ )
+}
+
+function PageItem({ node, pathname }: { node: SidebarNode; pathname: string }) {
+ const link = node.link ?? '#'
+ if (isExternal(link))
+ return (
+
+ {node.text}
+
+
+ )
+ return (
+ }>
+ {node.text}
+
+ )
+}
+
+/**
+ * The section's sidebar as DS SidebarMenu parts: labelled groups, page items, and submenus for
+ * collapsible entries. A submenu's trigger only toggles, so an entry with its own page lists it
+ * first as Overview.
+ */
+function SectionPages({ nodes, pathname }: { nodes: SidebarNode[]; pathname: string }) {
+ return nodes.map((node, index) => {
+ const key = `${node.text ?? node.link ?? 'node'}-${index}`
+ if (!node.items?.length) return
+ const overview = node.link ? (
+
+ ) : null
+ if (node.collapsed === undefined)
+ return (
+
+ {node.text ? {node.text} : null}
+ {overview}
+
+
+ )
+ return (
+
+ {node.text}
+
+ {overview}
+
+
+
+ )
+ })
+}
+
+function ThemeSelect() {
+ const [theme, setTheme] = useState('system')
+ useEffect(() => {
+ const sync = () => setTheme(localStorage.getItem('vocs-theme') ?? 'system')
+ sync()
+ const observer = new MutationObserver(sync)
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['data-vocs-theme'],
+ })
+ return () => observer.disconnect()
+ }, [])
+
+ return (
+
+ Appearance
+ {
+ const selected = event.target.value
+ const label = selected.charAt(0).toUpperCase() + selected.slice(1)
+ const control = document.querySelector(
+ `[data-v-sidebar-footer-content] [role="radio"][aria-label="${label} theme"]`,
+ )
+ if (control) control.click()
+ else {
+ localStorage.setItem('vocs-theme', selected)
+ const resolved =
+ selected === 'system'
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light'
+ : selected
+ document.documentElement.dataset.vocsTheme = resolved
+ document.documentElement.style.colorScheme = resolved
+ }
+ setTheme(selected)
+ }}
+ >
+ Light
+ Dark
+ System
+
+
+ )
+}
+
+/**
+ * The menu below 1200px, where the products leave the top bar: a DS Drawer with the products,
+ * Resources, then the current section's pages, Get API key and the theme.
+ */
+export default function DocsMenuDrawer({
+ pathname,
+ consoleHref,
+ consoleLabel,
+ consoleTitle,
+ disabled,
+ onSearch,
+}: {
+ pathname: string
+ consoleHref: string
+ consoleLabel: string
+ consoleTitle?: string
+ disabled?: boolean
+ onSearch: () => void
+}) {
+ const [open, setOpen] = useState(false)
+ const config = useConfig()
+ const active = getActiveDocsSection(pathname)
+ const pages = pathname === '/' ? [] : resolveSidebarItems(config?.sidebar, pathname)
+
+ // Close after navigation, including browser back and forward.
+ useEffect(() => {
+ setOpen(false)
+ }, [pathname])
+
+ // A widened viewport shows the products in the bar, so it must not keep the drawer open.
+ useEffect(() => {
+ if (!open) return
+ const wide = window.matchMedia('(min-width: 1200px)')
+ const close = () => {
+ if (wide.matches) setOpen(false)
+ }
+ close()
+ wide.addEventListener('change', close)
+ return () => wide.removeEventListener('change', close)
+ }, [open])
+
+ return (
+
+
+ }
+ >
+
+
+
+
+
+
+ {
+ setOpen(false)
+ requestAnimationFrame(onSearch)
+ }}
+ className={docsMenuDrawerSearch().className}
+ >
+
+ Search docs
+
+
+
+ {docsSections.map((section) => (
+
+ ))}
+
+
+ Resources
+ {docsResourceSections.map((section) => (
+
+ ))}
+
+
+ {pages.length > 0 ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+
+ {/* Absolute: the docs are also mounted at tempo.xyz/developers, where relative links miss the console. */}
+
+ {consoleLabel}
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/DocsNavigation.styles.ts b/src/components/DocsNavigation.styles.ts
index b90fa2516..24a7b4a1d 100644
--- a/src/components/DocsNavigation.styles.ts
+++ b/src/components/DocsNavigation.styles.ts
@@ -1,7 +1,11 @@
+import { global } from 'zyzz/web'
import { inherited } from '../styles/inherited'
import { style } from '../styles/scoped'
import { vars as tokens } from '../styles/theme'
+// The top bar: one 56 row with a bottom hairline. The lockup, the products as plain links
+// (from 1200px; below, they move into the menu drawer), search and Get API key.
+
export const docsSiteHeader = style({
position: 'fixed',
zIndex: tokens.zIndex.header,
@@ -10,13 +14,13 @@ export const docsSiteHeader = style({
insetInlineStart: 'max(0px, calc((100% - var(--tempo-docs-shell-width)) / 2))',
color: tokens.color.foreground,
backgroundColor: inherited.color.colorSurfaceShell,
- fontFamily: tokens.fontFamily.sansFallback,
+ fontFamily: tokens.fontFamily.ui,
selectors: {
'& :is(a, button, summary):focus-visible': {
outlineWidth: tokens.borderWidth.emphasis,
outlineStyle: 'solid',
outlineColor: tokens.color.foreground,
- outlineOffset: '4px',
+ outlineOffset: '2px',
},
},
'@media (prefers-reduced-motion: reduce)': {
@@ -28,767 +32,300 @@ export const docsSiteHeader = style({
},
})
+/** The top bar: full width, no pill. Brand and tabs on the left, search and the one CTA 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'],
- '@media (width < 380px)': {
- paddingInline: tokens.spacing['3_5'],
- },
- '@media (width >= 1080px)': {
- gap: tokens.spacing['5'],
- paddingInline: tokens.spacing['7'],
+ paddingInline: tokens.spacing['4'],
+ borderBottomWidth: tokens.borderWidth.hairline,
+ borderBottomStyle: 'solid',
+ borderBottomColor: tokens.color.line,
+ '@media (width >= 768px)': {
+ paddingInline: tokens.spacing['6'],
},
})
-export const docsHeaderBrand = style({
+export const docsHeaderLeft = style({
display: 'flex',
+ alignSelf: 'stretch',
alignItems: 'center',
- gap: tokens.spacing['7'],
+ gap: tokens.spacing['8'],
minWidth: 0,
- whiteSpace: 'nowrap',
- '@media (width < 380px)': {
- gap: tokens.spacing['5'],
- },
})
+/** TEMPO DEVELOPERS: the wordmark in content/primary, DEVELOPERS in content/secondary. */
export const docsHeaderLogo = style({
display: 'flex',
+ flexShrink: 0,
alignItems: 'center',
- height: '40px',
- color: 'inherit !custom',
+ gap: tokens.spacing['1_5'],
+ height: tokens.spacing['8'],
+ color: tokens.color.foreground,
+ textDecoration: 'none',
+ whiteSpace: 'nowrap',
})
-export const docsHeaderMobileSearch = style({
- display: 'flex',
- minWidth: 0,
- height: '39px',
- alignItems: 'center',
- gap: tokens.spacing['2_5'],
- borderWidth: tokens.borderWidth.hairline,
- borderStyle: 'solid',
- borderColor: 'transparent !custom',
- borderRadius: tokens.radius.md,
- '--corner-radius': tokens.radius.md,
- backgroundColor: inherited.color.surfaceInput,
-
- color: inherited.color.colorMixInSrgbColorForeground65Transparent,
- paddingInline: tokens.spacing['3_5'],
- fontSize: tokens.fontSize.sm,
- cursor: 'pointer',
- transitionProperty: 'color, background-color, border-color, opacity, transform',
- transitionDuration: 'var(--tempo-exit)',
- transitionTimingFunction: 'var(--tempo-ease)',
- selectors: {
- '&:hover': {
- // design-exception: Component artwork uses this optical mix; keep its existing contrast.
- borderColor: 'color-mix(in srgb, currentColor 25%, transparent) !custom',
- color: tokens.color.foreground,
- transitionDuration: 'var(--tempo-enter)',
- },
- },
- width: '100%',
- marginBottom: tokens.spacing['7'],
+// The outlined lockup at the Figma size (Tempo Alphabet 18).
+export const docsHeaderWordmark = style({
+ flexShrink: 0,
+ height: '15px',
+ width: '186px',
})
-export const docsHeaderActions = style({
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'flex-end',
- gap: tokens.spacing['1'],
- '@media (width >= 1080px)': {
- gap: tokens.spacing['2'],
- },
+export const docsHeaderDevelopers = style({
+ fill: tokens.color.muted,
})
-export const docsHeaderWebsite = style({
+/** The products, then Resources: plain links in one row from 1200px, where they fit beside search. */
+export const docsProductNav = style({
display: 'none',
- alignItems: 'center',
- gap: tokens.spacing['1'],
- fontSize: tokens.fontSize.xs,
-
- color: inherited.color.colorMixInSrgbColorForeground60Transparent,
- textDecoration: 'none',
+ alignSelf: 'stretch',
+ '@media (width >= 1200px)': {
+ display: 'block',
+ },
selectors: {
- '&:hover': {
- color: tokens.color.foreground,
+ '& ul': {
+ display: 'flex',
+ height: '100%',
+ alignItems: 'stretch',
+ margin: 0,
+ padding: 0,
+ listStyle: 'none',
+ },
+ '& li': {
+ display: 'flex',
},
- },
- '@media (width >= 1280px)': {
- display: 'flex',
},
})
-export const docsHeaderAgentMenu = style({
+// Product links read like DS Tabs: secondary text, the current one primary and medium with a
+// 2px indicator on the bar's bottom hairline. They stay links, since DS Tabs switch views.
+export const docsProductLink = style({
position: 'relative',
- display: 'none',
- '@media (width >= 1080px)': {
- display: 'block',
- },
-})
-
-// TDS Platform Button, secondary, small scale, with the G8 radius.
-export const docsHeaderAgentTrigger = style({
display: 'flex',
- height: '32px',
alignItems: 'center',
- gap: tokens.spacing['1_5'],
+ gap: tokens.spacing['1'],
+ paddingInline: tokens.spacing['2_5'],
border: 0,
- // G8: header controls were 12px; smoothed lg reads a little rounder.
- borderRadius: tokens.radius.lg,
- '--corner-radius': tokens.radius.lg,
- paddingInlineStart: tokens.spacing['4'],
- paddingInlineEnd: tokens.spacing['3'],
- color: inherited.color.vocsTextColorPrimary,
- backgroundColor: tokens.color.container,
+ background: 'none',
+ color: tokens.color.muted,
+ fontFamily: tokens.fontFamily.ui,
fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.control,
+ textDecoration: 'none',
whiteSpace: 'nowrap',
cursor: 'pointer',
transitionProperty: 'color, background-color, border-color, opacity, transform',
transitionDuration: 'var(--tempo-exit)',
transitionTimingFunction: 'var(--tempo-ease)',
selectors: {
- '&[aria-expanded="true"]': {
- backgroundColor: tokens.color.containerStrong,
+ '&::after': {
+ content: '""',
+ position: 'absolute',
+ insetInline: tokens.spacing['2_5'],
+ // design-exception: The indicator sits on the bar's 1px bottom hairline.
+ bottom: '-1px !custom',
+ height: '2px',
+ backgroundColor: 'transparent !custom',
+ },
+ '&:hover': {
+ color: tokens.color.foreground,
transitionDuration: 'var(--tempo-enter)',
},
- '&:disabled': {
- cursor: 'default',
+ '&:is([aria-current="page"], [data-active])': {
+ color: tokens.color.foreground,
+ fontWeight: tokens.fontWeight.medium,
},
- },
- '@media (hover: hover)': {
- selectors: {
- '&:hover': {
- backgroundColor: tokens.color.containerStrong,
- transitionDuration: 'var(--tempo-enter)',
- },
+ '&:is([aria-current="page"], [data-active])::after': {
+ backgroundColor: tokens.color.foreground,
+ },
+ '&[data-popup-open]': {
+ color: tokens.color.foreground,
},
},
})
-export const docsHeaderAgentPanel = style({
- position: 'absolute',
- insetInlineEnd: 0,
- top: 'calc(100% + 16px)',
- borderWidth: tokens.borderWidth.hairline,
- borderStyle: 'solid',
- borderColor: inherited.color.colorMixInSrgbCurrentColor12Transparent,
- borderRadius: tokens.radius.lg,
- '--corner-radius': tokens.radius.lg,
- backgroundColor: inherited.color.colorSurfacePage,
- // design-exception: Preserve this surface's layered artwork or focus treatment.
- boxShadow: '0 8px 24px #0000000d !custom',
- overflow: 'hidden',
- // G5: the panel fades and settles in, and fades out before it unmounts.
- transitionProperty: 'color, background-color, border-color, opacity, transform',
- transitionDuration: 'var(--tempo-enter)',
- transitionTimingFunction: 'var(--tempo-ease)',
- '@starting-style': {
- opacity: 0,
- transform: 'translateY(-4px)',
- },
- selectors: {
- '&[data-state="closed"]': {
- opacity: 0,
- transform: 'translateY(-4px)',
- transitionDuration: 'var(--tempo-exit)',
- },
- },
- '@media (prefers-reduced-motion: reduce)': {
- transitionProperty: 'opacity',
- transform: 'none',
- },
+export const docsProductChevron = style({
+ flexShrink: 0,
+ color: tokens.color.tertiary,
})
-export const docsHeaderMobileActions = style({
+export const docsHeaderActions = style({
display: 'flex',
+ flexShrink: 0,
alignItems: 'center',
- gap: tokens.spacing['0_5'],
- selectors: {
- '& .docs-header-destinations': {
- marginInlineEnd: tokens.spacing['2'],
- },
- },
- '@media (width >= 1080px)': {
- display: 'none',
- },
+ justifyContent: 'flex-end',
+ gap: tokens.spacing['2'],
})
-export const docsHeaderIconButton = style({
+/** Search: a 32 icon button below Display widths, a 240 field with its shortcut from 1376px. */
+export const docsHeaderSearchField = style({
display: 'grid',
- width: '36px',
- height: '36px',
+ width: tokens.spacing['8'],
+ height: tokens.spacing['8'],
placeItems: 'center',
- // G8: header controls were 12px; smoothed lg reads a little rounder.
- borderRadius: tokens.radius.lg,
- '--corner-radius': tokens.radius.lg,
- color: 'inherit !custom',
+ padding: tokens.spacing['0'],
+ border: 0,
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ background: 'none',
+ color: tokens.color.muted,
+ fontFamily: tokens.fontFamily.ui,
cursor: 'pointer',
transitionProperty: 'color, background-color, border-color, opacity, transform',
transitionDuration: 'var(--tempo-exit)',
transitionTimingFunction: 'var(--tempo-ease)',
selectors: {
'&:hover': {
- backgroundColor: inherited.color.colorMixInSrgbCurrentColor5Transparent,
- transitionDuration: 'var(--tempo-enter)',
- },
- },
-})
-
-export const docsHeaderMenuButton = style({
- '@media (width >= 1080px)': {
- display: 'none',
- },
-})
-
-export const docsSectionNav = style({
- selectors: {
- '& :is(a, button):focus-visible': {
- outlineWidth: tokens.borderWidth.emphasis,
- outlineStyle: 'solid',
- outlineColor: tokens.color.foreground,
- outlineOffset: '4px',
- },
- },
- display: 'flex',
- alignItems: 'center',
- color: tokens.color.foreground,
- backgroundColor: inherited.color.colorSurfaceShell,
- borderBottomWidth: tokens.borderWidth.hairline,
- borderBottomStyle: 'solid',
- borderBottomColor: inherited.color.colorMixInSrgbCurrentColor10Transparent,
- '@media (prefers-reduced-motion: reduce)': {
- selectors: {
- '& *': {
- transition: 'none',
- },
- },
- },
-})
-
-export const docsHeaderMobileDialog = style({
- position: 'fixed',
- inset: 0,
- width: '100%',
- height: '100dvh',
- maxWidth: 'none',
- maxHeight: 'none',
- margin: 0,
- padding: 0,
- border: 0,
- color: tokens.color.foreground,
- backgroundColor: inherited.color.colorSurfacePage,
- // G5: the menu fades in on open and out on close; display and overlay switch
- // discretely at the end so the exit is visible.
- opacity: 0,
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events',
- transitionDuration: 'var(--tempo-exit)',
- transitionTimingFunction: 'var(--tempo-ease)',
- transitionBehavior: 'allow-discrete',
- selectors: {
- '&:not([open])': {
- display: 'none',
- },
- '&[open]': {
- display: 'flex',
- flexDirection: 'column',
- opacity: 1,
+ color: tokens.color.foreground,
+ backgroundColor: tokens.color.containerSubtle,
transitionDuration: 'var(--tempo-enter)',
},
- '&::backdrop': {
- // design-exception: Preserve this surface's layered artwork or focus treatment.
- background: '#0006 !custom',
- },
},
- '@starting-style': {
+ '@media (width >= 1376px)': {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ gap: tokens.spacing['2'],
+ width: '240px',
+ paddingInline: tokens.spacing['3'],
+ backgroundColor: tokens.color.container,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
selectors: {
- '&[open]': {
- opacity: 0,
+ '&:hover': {
+ backgroundColor: tokens.color.containerStrong,
},
},
},
- '@media (prefers-reduced-motion: reduce)': {
- transitionProperty: 'none',
- },
})
-export const docsHeaderMobileTop = style({
- display: 'flex',
- flexShrink: 0,
- justifyContent: 'space-between',
- alignItems: 'center',
- height: 'var(--tempo-docs-primary-nav-height)',
- paddingInline: tokens.spacing['5'],
- borderBottomWidth: tokens.borderWidth.hairline,
- borderBottomStyle: 'solid',
- borderBottomColor: inherited.color.colorMixInSrgbCurrentColor10Transparent,
- '@media (width < 380px)': {
- paddingInline: tokens.spacing['3_5'],
+export const docsHeaderSearchLabel = style({
+ display: 'none',
+ '@media (width >= 1376px)': {
+ display: 'inline',
+ flex: 1,
+ color: tokens.color.tertiary,
+ textAlign: 'start',
},
})
-export const docsHeaderMobileBody = style({
- overflow: 'auto',
- overscrollBehavior: 'contain',
-
- paddingTop: tokens.spacing['5'],
- paddingInlineEnd: tokens.spacing['6'],
- paddingBottom: tokens.spacing['8'],
- paddingInlineStart: tokens.spacing['6'],
-})
-
-export const docsHeaderMobileLabel = style({
- marginBottom: tokens.spacing['4'],
-
- color: inherited.color.colorMixInSrgbColorForeground55Transparent,
- fontSize: tokens.fontSize.xs,
-})
-
-export const docsHeaderMobileSidebar = style({
- display: 'flex',
- flexDirection: 'column',
-})
-
-export const docsHeaderMobileSections = style({
- marginTop: tokens.spacing['6'],
- paddingTop: tokens.spacing['6'],
- borderTopWidth: tokens.borderWidth.hairline,
- borderTopStyle: 'solid',
- borderTopColor: inherited.color.colorMixInSrgbCurrentColor10Transparent,
- selectors: {
- '& a': {
- display: 'flex',
- justifyContent: 'space-between',
-
- paddingBlock: tokens.spacing['2'],
- fontSize: tokens.fontSize.sm,
- color: 'inherit !custom',
- textDecoration: 'none',
- },
- '& a span': {
- opacity: 0.4,
- },
+export const docsHeaderKbd = style({
+ display: 'none',
+ '@media (width >= 1376px)': {
+ display: 'inline-flex',
+ alignItems: 'center',
+ paddingInline: tokens.spacing['1'],
+ borderRadius: tokens.radius.sm,
+ backgroundColor: tokens.color.container,
+ color: tokens.color.tertiary,
+ fontFamily: tokens.fontFamily.ui,
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
},
})
-export const docsHeaderMobileResources = style({
- marginTop: tokens.spacing['6'],
- paddingTop: tokens.spacing['6'],
- borderTopWidth: tokens.borderWidth.hairline,
- borderTopStyle: 'solid',
- borderTopColor: inherited.color.colorMixInSrgbCurrentColor10Transparent,
- selectors: {
- '& > .docs-resource-links': {
- gap: tokens.spacing['0'],
- },
- '& > .docs-resource-links a': {
- paddingBlock: tokens.spacing['2'],
- paddingInline: tokens.spacing['0'],
- },
- '& > .docs-resource-links a:hover': {
- backgroundColor: 'transparent !custom',
- },
- '& > .docs-resource-links svg:last-child': {
- opacity: 0.4,
- },
+/** Get API key ("Console" when signed in), the one primary button: a DS Button from Tablet up. */
+export const docsHeaderGetKey = style({
+ display: 'none',
+ '@media (width >= 768px)': {
+ display: 'contents',
},
})
-export const docsHeaderMobileAgents = style({
- marginTop: tokens.spacing['6'],
- paddingTop: tokens.spacing['6'],
- borderTopWidth: tokens.borderWidth.hairline,
- borderTopStyle: 'solid',
- borderTopColor: inherited.color.colorMixInSrgbCurrentColor10Transparent,
- selectors: {
- '& summary': {
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- paddingBlock: tokens.spacing['2'],
- color: tokens.color.foreground,
- fontSize: tokens.fontSize.sm,
- listStyle: 'none',
- cursor: 'pointer',
- },
- '& summary::-webkit-details-marker': {
- display: 'none',
- },
- '& summary svg': {
- opacity: 0.4,
- transition: 'rotate 150ms',
- },
- '&[open] summary': {
- marginBottom: tokens.spacing['4'],
- },
- '&[open] summary svg': {
- rotate: '180deg',
- },
- },
- '@media (prefers-reduced-motion: reduce)': {
- selectors: {
- '& summary svg': {
- transition: 'none',
- },
- },
+/** The menu button opens the drawer below 1200px, where the products leave the bar. */
+export const docsHeaderMenuButton = style({
+ display: 'contents',
+ '@media (width >= 1200px)': {
+ display: 'none',
},
})
-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,
- },
+// The DS drawer's layers have no z-index, so the fixed top bar would cover them; while the
+// drawer is mounted the bar drops back into the page's stacking order.
+global({
+ 'body:has(.docs-menu-drawer) .docs-site-header': {
+ zIndex: 'auto',
},
})
-export const docsHeaderMobileWebsite = style({
+/** The menu drawer: products, Resources, then the current section's pages. */
+export const docsMenuDrawerPopup = style({
display: 'flex',
- alignItems: 'center',
- gap: tokens.spacing['2'],
- marginTop: tokens.spacing['6'],
-
- color: inherited.color.colorMixInSrgbColorForeground55Transparent,
- fontSize: tokens.fontSize.xs,
- textDecoration: 'none',
-})
-
-export const docsSectionNavScroll = style({
- flex: 1,
- minWidth: 0,
- height: 'var(--tempo-docs-section-nav-height)',
- overflowX: 'auto',
- paddingInline: tokens.spacing['5'],
- scrollbarWidth: 'none',
- // design-exception: Fade the scroll edges so clipped section tabs read as scrollable instead of cut off.
- maskImage:
- 'linear-gradient(to right, transparent, black 16px, black calc(100% - 24px), transparent) !custom',
- selectors: {
- '&::-webkit-scrollbar': {
- display: 'none',
- },
- '& ul': {
- display: 'flex',
- width: 'max-content',
- minWidth: '100%',
- height: '100%',
- alignItems: 'center',
- gap: tokens.spacing['1_5'],
- margin: 0,
- padding: 0,
- listStyle: 'none',
- },
- '& a': {
- position: 'relative',
- display: 'flex',
- alignItems: 'center',
-
- gap: tokens.spacing['2'],
- height: '34px',
-
- paddingBlock: tokens.spacing['0'],
- paddingInline: tokens.spacing['2_5'],
- border: 0,
- borderRadius: tokens.radius.lg,
-
- color: inherited.color.colorMixInSrgbColorForeground65Transparent,
- fontSize: tokens.fontSize.compact,
- fontWeight: tokens.fontWeight.medium,
- textDecoration: 'none',
- whiteSpace: 'nowrap',
- transition: 'color 150ms, background-color 150ms',
- },
- '& a:hover': {
- color: tokens.color.foreground,
- backgroundColor: inherited.color.colorMixInSrgbColorForeground3Transparent,
- },
- '& a[aria-current="page"]': {
- color: tokens.color.foreground,
- backgroundColor: inherited.color.colorMixInSrgbColorForeground8ColorSurfaceShell,
- },
- },
- '@media (width >= 1080px)': {
- paddingInline: tokens.spacing['7'],
- },
-})
-
-export const docsSectionIcon = style({
- flexShrink: 0,
+ flexDirection: 'column',
+ gap: tokens.spacing['4'],
+ width: 'min(360px, 100vw)',
+ // design-exception: The drawer's own 32px padding is tighter here, so labels sit on the 16px gutter.
+ padding: '16px 12px 24px !custom',
+ fontFamily: tokens.fontFamily.ui,
})
-export const docsSectionUtilities = style({
+export const docsMenuDrawerTitle = 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',
+ minHeight: tokens.spacing['8'],
+ margin: 0,
+ paddingInline: tokens.spacing['2'],
})
-export const docsReferenceTrigger = style({
- position: 'relative',
+export const docsMenuDrawerSearch = style({
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',
+ gap: tokens.spacing['2'],
+ width: '100%',
+ height: tokens.spacing['8'],
+ paddingInline: tokens.spacing['3'],
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',
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ backgroundColor: tokens.color.container,
+ color: tokens.color.tertiary,
+ fontFamily: tokens.fontFamily.ui,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
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 docsMenuDrawerDivider = style({
+ margin: 0,
+ border: 0,
+ borderTopWidth: tokens.borderWidth.hairline,
+ borderTopStyle: 'solid',
+ borderTopColor: tokens.color.line,
})
-export const docsResourceLinks = style({
+export const docsMenuDrawerFooter = style({
display: 'grid',
- gridTemplateColumns: '1fr',
- gap: tokens.spacing['1'],
- fontFamily: tokens.fontFamily.sansFallback,
- selectors: {
- '& a': {
- display: 'block',
-
- paddingBlock: tokens.spacing['2_5'],
- paddingInline: tokens.spacing['3'],
- borderRadius: tokens.radius.md,
- color: tokens.color.foreground,
- fontSize: tokens.fontSize.sm,
- lineHeight: tokens.lineHeight.snug,
- textDecoration: 'none',
- },
- '& a:hover': {
- backgroundColor: inherited.color.colorSurfaceBlock,
- },
- },
-})
-
-export const docsResourceLinkLabel = style({
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
gap: tokens.spacing['3'],
+ marginTop: 'auto !custom',
+ paddingInline: tokens.spacing['1'],
})
-export const docsResourceLogo = style({
- width: '16px',
- height: '16px',
- flexShrink: 0,
-})
-
-export const docsResourceLinkText = style({
- flex: 1,
-})
-
-export const docsHeaderMobileTheme = style({
+export const docsMenuDrawerTheme = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
- borderTopWidth: tokens.borderWidth.hairline,
- borderTopStyle: 'solid',
- borderTopColor: inherited.color.colorMixInSrgbCurrentColor10Transparent,
- marginTop: tokens.spacing['6'],
- paddingTop: tokens.spacing['6'],
- fontSize: tokens.fontSize.compact,
+ paddingInlineStart: tokens.spacing['2'],
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
selectors: {
'& select': {
+ height: tokens.spacing['8'],
+ paddingInline: tokens.spacing['2'],
borderWidth: tokens.borderWidth.hairline,
borderStyle: 'solid',
- borderColor: inherited.color.colorMixInSrgbCurrentColor16Transparent,
- borderRadius: tokens.radius.lg,
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- padding: '7px 10px !custom',
+ borderColor: tokens.color.line,
+ borderRadius: tokens.radius.md,
backgroundColor: inherited.color.colorSurfaceShell,
- color: 'inherit !custom',
- font: 'inherit',
- },
- },
-})
-
-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',
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap',
- // Below the full header the link shows only its icon; its aria-label and title carry the email.
- '@media (width < 1080px)': {
- display: 'none',
- },
-})
-
-/** 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,
- },
+ font: 'inherit',
},
},
})
diff --git a/src/components/DocsNotFound.recipes.ts b/src/components/DocsNotFound.recipes.ts
new file mode 100644
index 000000000..43bdc41f8
--- /dev/null
+++ b/src/components/DocsNotFound.recipes.ts
@@ -0,0 +1,119 @@
+import { style } from '../styles/recipes'
+import { vars as tokens } from '../styles/theme'
+
+// One column at the prose measure: title, lead, Search docs, then the top tasks. The
+// article supplies the page's top and bottom padding.
+export const page = style({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ width: '100%',
+ maxWidth: '720px',
+ marginInline: 'auto !custom',
+ fontFamily: tokens.fontFamily.ui,
+})
+
+export const title = style({
+ fontFamily: tokens.fontFamily.book,
+ fontSize: tokens.fontSize.heading,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.heading,
+ letterSpacing: tokens.letterSpacing.tight,
+ color: tokens.color.foreground,
+ '@media (width >= 768px)': {
+ fontSize: tokens.fontSize.displaySmall,
+ },
+})
+
+// H1 → lead 12; lead → first content 32.
+export const lede = style({
+ marginBlockStart: tokens.spacing['3'],
+ fontSize: tokens.fontSize.lead,
+ lineHeight: tokens.lineHeight.relaxed,
+ color: tokens.color.muted,
+ textWrap: 'pretty',
+})
+
+// A secondary button: the top bar's Get API key stays the view's one primary button.
+export const searchButton = style({
+ marginBlockStart: tokens.spacing['8'],
+})
+
+export const section = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: tokens.spacing['3'],
+ width: '100%',
+ marginBlockStart: tokens.spacing['12'],
+})
+
+export const sectionTitle = style({
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.control,
+ color: tokens.color.muted,
+})
+
+// One surface (12, inset 4) holding the task rows (8 = 12 − 4).
+export const taskList = style({
+ display: 'flex',
+ flexDirection: 'column',
+ padding: tokens.spacing['1'],
+ borderWidth: tokens.borderWidth.hairline,
+ borderStyle: 'solid',
+ borderColor: tokens.color.hairline,
+ borderRadius: tokens.radius.lg,
+ '--corner-radius': tokens.radius.lg,
+})
+
+export const taskLink = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: tokens.spacing['3'],
+ paddingBlock: tokens.spacing['3'],
+ paddingInline: tokens.spacing['3'],
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ color: tokens.color.foreground,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ textDecoration: 'none',
+ transitionProperty: 'color, background-color, border-color, opacity, transform',
+ transitionDuration: 'var(--tempo-exit)',
+ transitionTimingFunction: 'var(--tempo-ease)',
+ selectors: {
+ '&:hover': {
+ '@media (hover: hover)': {
+ backgroundColor: tokens.color.containerSubtle,
+ transitionDuration: 'var(--tempo-enter)',
+ },
+ },
+ },
+ '@media (prefers-reduced-motion: reduce)': {
+ transitionProperty: 'none',
+ },
+})
+
+// A 32 icon tile (radius 8) with a 16 icon.
+export const taskTile = style({
+ display: 'inline-flex',
+ flexShrink: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '32px',
+ height: '32px',
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ backgroundColor: tokens.color.container,
+ color: tokens.color.foreground,
+})
+
+export const taskLabel = style({
+ flexGrow: 1,
+ minWidth: 0,
+})
+
+export const taskChevron = style({
+ flexShrink: 0,
+ color: tokens.color.muted,
+})
diff --git a/src/components/DocsNotFound.tsx b/src/components/DocsNotFound.tsx
new file mode 100644
index 000000000..34d07e4a2
--- /dev/null
+++ b/src/components/DocsNotFound.tsx
@@ -0,0 +1,96 @@
+'use client'
+
+import { Layout, MdxPageContextProvider } from 'vocs'
+import { Link, useRouter } from 'waku'
+import ChevronRightIcon from '~icons/lucide/chevron-right'
+import KeyRoundIcon from '~icons/lucide/key-round'
+import NetworkIcon from '~icons/lucide/network'
+import SearchIcon from '~icons/lucide/search'
+import SendIcon from '~icons/lucide/send'
+import ServerIcon from '~icons/lucide/server'
+import DocsLayout from '../pages/docs/_layout'
+import { button } from '../styles/controls'
+import * as ui from './DocsNotFound.recipes'
+
+const topTasks = [
+ { label: 'Send your first payment', href: '/get-started/quickstart', Icon: SendIcon },
+ { label: 'Create an API key', href: '/docs/api/console/api-keys', Icon: KeyRoundIcon },
+ { label: 'Network details', href: '/docs/quickstart/connection-details', Icon: NetworkIcon },
+ { label: 'Run a node', href: '/docs/guide/node', Icon: ServerIcon },
+]
+
+// Vocs' search listens for Cmd/Ctrl+K on the document; the docs header opens it the same way.
+function openDocsSearch() {
+ document.dispatchEvent(
+ new KeyboardEvent('keydown', {
+ key: 'k',
+ code: 'KeyK',
+ bubbles: true,
+ cancelable: true,
+ metaKey: true,
+ ctrlKey: true,
+ }),
+ )
+}
+
+// Search and the four top tasks; the nav already lists the areas.
+function NotFoundContent() {
+ return (
+
+
+
Page not found
+
+ This page doesn't exist or has moved. Search the docs, or start from a common task.
+
+
+
+ Search docs
+
+
+
+ Top tasks
+
+
+ {topTasks.map(({ label, href, Icon }) => (
+
+
+
+
+
+ {label}
+
+
+
+ ))}
+
+
+
+ )
+}
+
+/** The 404 page inside the docs shell: header and the Full footer around the content. */
+export default function DocsNotFound() {
+ const { path } = useRouter()
+ const page = (
+
+
+
+
+
+ )
+ // The entry layout already wraps Get Started routes in the docs shell.
+ const inShell = /^(?:\/developers)?\/get-started(?:\/|$)/.test(path ?? '')
+ return inShell ? page : {page}
+}
diff --git a/src/components/DocsPageActions.styles.ts b/src/components/DocsPageActions.styles.ts
index fd5c376bf..1a6f7c6d7 100644
--- a/src/components/DocsPageActions.styles.ts
+++ b/src/components/DocsPageActions.styles.ts
@@ -1,5 +1,4 @@
import { global } from 'zyzz/web'
-import { inherited } from '../styles/inherited'
import { style } from '../styles/scoped'
import { vars as tokens } from '../styles/theme'
@@ -8,88 +7,31 @@ global({
'[data-layout]:has(.docs-page-actions) [data-v-copy-for-ai]': {
display: 'none',
},
+ // The header row sits above the H1, across the content column.
+ 'article[data-v-content] .docs-page-header-host': {
+ display: 'block',
+ marginBottom: tokens.spacing['6'],
+ },
})
-export const docsPageActions = style({
+/** The breadcrumb on the left and the page actions on the right, wrapping on narrow screens. */
+export const docsPageHeader = style({
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
- gap: tokens.spacing['2'],
- paddingTop: tokens.spacing['1'],
- paddingBottom: tokens.spacing['6'],
-
- marginBottom: tokens.spacing['8'],
- fontFamily: tokens.fontFamily.sansFallback,
- fontSize: tokens.fontSize.compact,
- lineHeight: tokens.lineHeight.normal,
- selectors: {
- '& :is(a, button)': {
- display: 'inline-flex',
- alignItems: 'center',
- gap: tokens.spacing['1_5'],
- minHeight: '32px',
- paddingBlock: tokens.spacing['1_5'],
- paddingInline: tokens.spacing['3'],
- border: 0,
- // G8: secondary actions use the Platform gray fill; one radius, smoothed.
- borderRadius: tokens.radius.md,
- '--corner-radius': tokens.radius.md,
- backgroundColor: tokens.color.container,
-
- color: tokens.color.foreground,
- fontFamily: 'inherit !custom',
- fontSize: 'inherit !custom',
- textDecoration: 'none',
- cursor: 'pointer',
- transitionProperty: 'color, background-color, border-color, opacity, transform',
- transitionDuration: 'var(--tempo-exit)',
- transitionTimingFunction: 'var(--tempo-ease)',
- },
- '& :is(a, button):hover': {
- backgroundColor: tokens.color.containerStrong,
- transitionDuration: 'var(--tempo-enter)',
- },
- '& > button:first-child': {
- backgroundColor: inherited.color.backgroundColorInvert,
-
- color: inherited.color.textColorInvert,
- },
- '& > button:first-child:hover': {
- backgroundColor: inherited.color.backgroundColorInvert,
- opacity: 0.9,
- },
- },
- '@media (prefers-reduced-motion: reduce)': {
- selectors: {
- '& :is(a, button)': {
- transition: 'none',
- },
- },
- },
- '@media (width < 600px)': {
- marginBottom: tokens.spacing['6'],
- selectors: {
- // Three labeled buttons do not fit one phone-width row; keep the primary labeled and
- // collapse the secondary actions to icons with their labels still exposed to readers.
- '& > :is(a, button):not(:first-child)': {
- justifyContent: 'center',
- minWidth: '32px',
- paddingInline: tokens.spacing['2'],
- },
- '& > :is(a, button):not(:first-child) .docs-page-action-label': {
- position: 'absolute',
- width: '1px',
- height: '1px',
- overflow: 'hidden',
- clipPath: 'inset(50%)',
- whiteSpace: 'nowrap',
- },
- },
- },
+ justifyContent: 'space-between',
+ gap: tokens.spacing['3'],
+ minHeight: tokens.spacing['8'],
+ fontFamily: tokens.fontFamily.ui,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ letterSpacing: tokens.letterSpacing.normal,
+ lineHeight: tokens.lineHeight.control,
})
-export const docsPageActionsError = style({
- width: '100%',
-
- color: inherited.color.vocsTextColorMuted,
+export const docsPageActions = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: tokens.spacing['2'],
+ marginInlineStart: 'auto !custom',
})
diff --git a/src/components/DocsPageActions.tsx b/src/components/DocsPageActions.tsx
index 93fc526b1..1161fc869 100644
--- a/src/components/DocsPageActions.tsx
+++ b/src/components/DocsPageActions.tsx
@@ -1,23 +1,124 @@
'use client'
-import { useEffect, useRef, useState } from 'react'
+import { Breadcrumbs, Button, Menu, SplitButton } from '@tempoxyz/ds'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
-import { Link } from 'waku'
+import { useConfig } from 'vocs'
+import BotIcon from '~icons/lucide/bot'
import CheckIcon from '~icons/lucide/check'
import CopyIcon from '~icons/lucide/copy'
-import DownloadIcon from '~icons/lucide/download'
-import ExternalLinkIcon from '~icons/lucide/external-link'
+import FileTextIcon from '~icons/lucide/file-text'
+import SparklesIcon from '~icons/lucide/sparkles'
import GitHubIcon from '~icons/simple-icons/github'
+import { getActiveDocsSection } from '../lib/docs-sections'
+import { normalizeDocsPath, resolveSidebarItems, type SidebarNode } from '../lib/docs-sidebar'
import { publicAssetPath } from '../lib/public-asset-path'
-import { CopyButton, type CopyButtonState } from './CopyButton'
-import { CopyIconSwap } from './CopyIconSwap'
import { usePathname } from './DocsHeader'
import * as ui from './DocsPageActions.recipes'
-import { docsPageActions, docsPageActionsError } from './DocsPageActions.styles'
+import { docsPageActions, docsPageHeader } from './DocsPageActions.styles'
+import { RouterLink } from './RouterLink'
-/** Place page tools after the article introduction, including after client navigation. */
+type CopyState = 'idle' | 'copied' | 'error'
+
+/**
+ * The sidebar label of the current page, for the breadcrumb's last item. A generic "Overview"
+ * takes its group's name instead, such as Earn API.
+ */
+function activePageLabel(
+ nodes: SidebarNode[],
+ pathname: string,
+ group?: string,
+): string | undefined {
+ for (const node of nodes) {
+ const link = node.link?.split('#')[0]
+ if (link?.startsWith('/') && normalizeDocsPath(link).replace(/\/+$/, '') === pathname)
+ return node.text === 'Overview' && group ? group : node.text
+ const child = activePageLabel(node.items ?? [], pathname, node.text)
+ if (child) return child
+ }
+}
+
+/** Below Tablet the actions keep only their icons, so the header row stays one line. */
+function useCompact() {
+ const [compact, setCompact] = useState(false)
+ useEffect(() => {
+ const query = window.matchMedia('(width < 768px)')
+ const update = () => setCompact(query.matches)
+ update()
+ query.addEventListener('change', update)
+ return () => query.removeEventListener('change', update)
+ }, [])
+ return compact
+}
+
+/** Ask AI: opens this page in ChatGPT or Claude. ⌘I toggles it, in place of Vocs' docked field. */
+function AskAiMenu({ pathname, compact }: { pathname: string; compact: boolean }) {
+ const [open, setOpen] = useState(false)
+
+ useEffect(() => {
+ // Capture runs before Vocs' own window listener, whose field is hidden (see globals.ts).
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== 'i') return
+ event.preventDefault()
+ event.stopImmediatePropagation()
+ setOpen((value) => !value)
+ }
+ window.addEventListener('keydown', onKeyDown, true)
+ return () => window.removeEventListener('keydown', onKeyDown, true)
+ }, [])
+
+ const providers = useMemo(() => {
+ if (typeof window === 'undefined') return []
+ const pageUrl = `${window.location.origin}${pathname}`
+ const query = encodeURIComponent(
+ `Please research and analyze this page: ${pageUrl} so I can ask you questions about it. Once you have read it, prompt me with any questions I have. Do not post content from the page in your response. Any of my follow up questions must reference the site I gave you.`,
+ )
+ return [
+ { name: 'Ask ChatGPT', href: `https://chatgpt.com?hints=search&q=${query}` },
+ { name: 'Ask Claude', href: `https://claude.ai/new?q=${query}` },
+ ]
+ }, [pathname])
+
+ return (
+
+
+ }
+ >
+
+ {compact ? null : 'Ask AI'}
+
+
+ {providers.map((provider) => (
+
+ {provider.name}
+
+ ))}
+
+
+ )
+}
+
+/**
+ * The page's header row, above the H1, including after client navigation: the Section / Page
+ * breadcrumb, Ask AI, and Copy page with its menu (View as Markdown, Connect your agent).
+ */
export default function DocsPageActions({ openApi = false }: { openApi?: boolean }) {
const pathname = usePathname()
+ const config = useConfig()
+ const compact = useCompact()
const isApiArticle = pathname === '/docs/api' || pathname.startsWith('/docs/api/')
const isDocsArticle =
(pathname.startsWith('/docs/') ||
@@ -25,9 +126,13 @@ export default function DocsPageActions({ openApi = false }: { openApi?: boolean
pathname.startsWith('/get-started/')) &&
openApi === isApiArticle
const [host, setHost] = useState(null)
- const [state, setState] = useState('idle')
+ const [state, setState] = useState('idle')
const marker = useRef(null)
+ const resetTimer = useRef | null>(null)
const markdownPath = publicAssetPath(`/assets/md${pathname.replace(/\/$/, '')}.md`)
+ const section = getActiveDocsSection(pathname)
+ const current = normalizeDocsPath(pathname).replace(/\/+$/, '') || '/'
+ const pageLabel = activePageLabel(resolveSidebarItems(config?.sidebar, pathname), current)
useEffect(() => {
setState('idle')
@@ -46,15 +151,16 @@ export default function DocsPageActions({ openApi = false }: { openApi?: boolean
)
: article
const heading = content?.querySelector('h1')
- if (!heading || content?.querySelector('.tempo-docs-home')) return
- const introduction =
- heading.closest('.docs-product-overview, [data-v-openapi-header]') ??
- (heading.nextElementSibling?.matches('p') ? heading.nextElementSibling : heading)
- if (element?.isConnected && element.previousElementSibling === introduction) return
+ if (!content || !heading || content.querySelector('.tempo-docs-home')) return
+ // The row spans the content column, so it goes before the H1's top-level block, such as a
+ // product hero that sets the H1 beside an illustration.
+ let anchor: Element = heading
+ while (anchor.parentElement && anchor.parentElement !== content) anchor = anchor.parentElement
+ if (element?.isConnected && element.nextElementSibling === anchor) return
element?.remove()
element = document.createElement('div')
- element.className = 'docs-page-actions-host'
- introduction.after(element)
+ element.className = 'docs-page-header-host'
+ anchor.before(element)
setHost(element)
}
attach()
@@ -67,12 +173,26 @@ export default function DocsPageActions({ openApi = false }: { openApi?: boolean
}
}, [pathname, isDocsArticle, openApi])
- const loadMarkdown = async () => {
- const response = await fetch(markdownPath)
- if (!response.ok) throw new Error('Could not load page Markdown')
- const markdown = await response.text()
- if (/^\s* () => {
+ if (resetTimer.current) clearTimeout(resetTimer.current)
+ },
+ [],
+ )
+
+ const copyPage = async () => {
+ if (resetTimer.current) clearTimeout(resetTimer.current)
+ try {
+ const response = await fetch(markdownPath)
+ if (!response.ok) throw new Error('Could not load page Markdown')
+ const markdown = await response.text()
+ if (/^\s* setState('idle'), 1200)
+ } catch {
+ setState('error')
+ }
}
if (!isDocsArticle) return null
@@ -82,61 +202,71 @@ export default function DocsPageActions({ openApi = false }: { openApi?: boolean
{host &&
createPortal(
-
- }
- checkIcon={ }
- />
- }
- resetAfter={1200}
- onStateChange={setState}
- />
-
-
- View Markdown
-
-
-
- Install agent tools
-
- {pathname.replace(/\/$/, '') === '/docs/earn' && (
-
-
- GitHub
-
- )}
-
- {state === 'error'
- ? 'Could not copy. Open View Markdown to select the page text.'
- : state === 'copied'
- ? 'Page Markdown copied to clipboard.'
- : ''}
-
- ,
+
+
+ {section ? (
+ }>
+ {section.label}
+
+ ) : null}
+ {pageLabel ? {pageLabel} : null}
+
+
+
+
+
+ {state === 'copied' ? (
+
+ ) : (
+
+ )}
+ {compact ? null : 'Copy page'}
+
+
+
+
+
+
+ View as Markdown
+
+ }
+ >
+
+ Connect your agent
+
+ {pathname.replace(/\/$/, '') === '/docs/earn' && (
+
+
+ View Earn on GitHub
+
+ )}
+
+
+
+
+ {state === 'error'
+ ? 'Could not copy. Open View as Markdown to select the page text.'
+ : state === 'copied'
+ ? 'Page Markdown copied to clipboard.'
+ : ''}
+
+
+
,
host,
)}
>
diff --git a/src/components/DocsProductOverview.styles.ts b/src/components/DocsProductOverview.styles.ts
index 4cb9b19bf..0da3f0c37 100644
--- a/src/components/DocsProductOverview.styles.ts
+++ b/src/components/DocsProductOverview.styles.ts
@@ -5,15 +5,11 @@ import { vars as tokens } from '../styles/theme'
// Document/Vocs integration selectors cannot be attached to owned elements.
global({
+ // The docs H1 rule sets the size (32, 28 below 768), weight and tracking.
'article[data-v-content] .docs-product-overview-copy h1[data-v]': {
margin: 0,
padding: 0,
border: 0,
- // design-exception: Preserve this responsive geometry across viewport sizes.
- fontSize: 'clamp(32px, 4.5cqi, 42px) !custom',
- letterSpacing: tokens.letterSpacing.display,
-
- lineHeight: tokens.lineHeight.display,
},
'article[data-v-content] .docs-product-overview-copy p[data-v]': {
// design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
@@ -21,10 +17,13 @@ global({
fontSize: tokens.fontSize.body,
lineHeight: tokens.lineHeight.relaxed,
},
+ // The lead matches every docs page: 18/28 secondary, 12 below the H1.
'article[data-v-content] .docs-product-overview-copy h1[data-v] + p[data-v]': {
- color: inherited.color.colorMixInSrgbForeground68Transparent,
-
- fontSize: tokens.fontSize.body,
+ marginBlockStart: tokens.spacing['3'],
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.lead,
+ // design-exception: Lead 18/28 (system-v2); contract.ts has no 28px line height.
+ lineHeight: '28px !custom',
},
// Generated illustrations (scripts/illustrations) read these tokens; Earn guide figures share them.
':where(.dark, [data-vocs-theme="dark"], [style*="color-scheme: dark"]) :is(.docs-product-overview-art, .docs-earn-art)':
@@ -68,8 +67,7 @@ export const docsProductOverviewArt = style({
margin: 0,
justifySelf: 'end',
overflow: 'hidden',
-
- borderRadius: tokens.radius.xl,
+ borderRadius: tokens.radius.lg,
selectors: {
'& svg': {
display: 'block',
diff --git a/src/components/DocsSectionNav.tsx b/src/components/DocsSectionNav.tsx
deleted file mode 100644
index 4ae93ffd6..000000000
--- a/src/components/DocsSectionNav.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-'use client'
-
-import { useEffect, useId, useRef, useState } from 'react'
-import { Link, useRouter } from 'waku'
-import ArrowUpRightIcon from '~icons/lucide/arrow-up-right'
-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 TerminalIcon from '~icons/lucide/terminal'
-import {
- type DocsSection,
- docsSections,
- docsUtilitySections,
- getActiveDocsSection,
-} from '../lib/docs-sections'
-import { docsSectionNav as sectionSurface } from '../styles/surfaces.styles'
-import { docsProductIcons } from './DocsHomeProductIcon'
-import {
- docsApiMenuItem,
- docsApiMenuMobile,
- docsReferenceMenu,
- docsReferencePanel,
- docsReferenceTrigger,
- docsResourceLinkLabel,
- docsResourceLinks,
- docsResourceLinkText,
- docsResourceLogo,
- docsSectionIcon,
- docsSectionNav,
- docsSectionNavScroll,
- docsSectionUtilities,
- docsSectionUtilityLink,
-} from './DocsNavigation.styles'
-import { MercatorLogo, MppLogo, TempoMark } from './ToolLogos'
-
-export { docsSections, docsUtilitySections, getActiveDocsSection }
-
-const sectionIcons: Partial> = {
- overview: CompassIcon,
- accounts: docsProductIcons.accounts,
- earn: docsProductIcons.earn,
- routes: docsProductIcons.routes,
- zones: docsProductIcons.zones,
- 'machine-payments': docsProductIcons.agents,
- developers: docsProductIcons.network,
-}
-
-export const docsExternalTools = [
- { label: 'MPP', href: 'https://mpp.dev/', logo: MppLogo },
- { label: 'Mercator', href: 'https://mercator.sh/', logo: MercatorLogo },
- { label: 'Tempo Console', href: 'https://console.tempo.xyz/', logo: TempoMark },
- { label: 'Tempo Wallet', href: 'https://wallet.tempo.xyz/', logo: TempoMark },
-]
-
-export function DocsResourceLinks({ onNavigate }: { onNavigate?: () => void }) {
- return (
-
- {docsExternalTools.map((tool) => (
-
-
-
-
- {tool.label}
-
-
-
-
- ))}
-
- )
-}
-
-export function DocsApiDropdown({
- active,
- mobile = false,
- onNavigate,
-}: {
- active: boolean
- mobile?: boolean
- onNavigate?: () => void
-}) {
- const [open, setOpen] = useState(false)
- const ref = useRef(null)
- const buttonRef = useRef(null)
- const panelId = useId()
- const { path } = useRouter()
-
- useEffect(() => {
- setOpen(false)
- }, [path])
- useEffect(() => {
- if (!open) return
- const dismiss = (event: PointerEvent) => {
- if (!ref.current?.contains(event.target as Node)) setOpen(false)
- }
- document.addEventListener('pointerdown', dismiss)
- return () => document.removeEventListener('pointerdown', dismiss)
- }, [open])
-
- return (
- // biome-ignore lint/a11y/useSemanticElements: This groups a navigation disclosure, not form controls.
- {
- if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false)
- }}
- onKeyDown={(event) => {
- if (event.key === 'Escape' && open) {
- event.preventDefault()
- event.stopPropagation()
- setOpen(false)
- buttonRef.current?.focus()
- }
- }}
- >
-
setOpen(!open)}
- >
- SDKs & CLI
-
-
- {[
- { label: 'Overview', href: '/docs/sdk', icon: CompassIcon },
- { label: 'SDKs', href: '/docs/sdk', icon: CodeIcon },
- { label: 'CLI', href: '/docs/cli', icon: TerminalIcon },
- ].map((item) => (
- {
- setOpen(false)
- onNavigate?.()
- }}
- >
-
-
- {item.label}
-
-
- ))}
- {
- setOpen(false)
- onNavigate?.()
- }}
- >
-
-
- Explorer
-
-
-
-
-
- )
-}
-
-export default function DocsSectionNav() {
- const { path } = useRouter()
- const navRef = useRef(null)
- const activeLinkRef = useRef(null)
- const activeSection = getActiveDocsSection(path ?? '/')
- const activeLabel = activeSection?.label ?? null
-
- useEffect(() => {
- if (!activeLabel || !docsSections.some((section) => section.id === activeSection?.id)) return
- const nav = navRef.current
- const activeLink = activeLinkRef.current
- if (!nav || !activeLink) return
- const centeredLeft = activeLink.offsetLeft - nav.clientWidth / 2 + activeLink.offsetWidth / 2
- nav.scrollTo({ left: Math.max(0, centeredLeft), behavior: 'instant' })
- }, [activeLabel, activeSection?.id])
-
- return (
-
-
-
- {docsSections.map((item) => {
- const Icon = sectionIcons[item.id]
- return (
-
- {
- if (activeSection?.id === item.id) activeLinkRef.current = element
- }}
- to={item.href}
- unstable_prefetchOnEnter={{}}
- unstable_prefetchOnView={{}}
- aria-current={activeSection?.id === item.id ? 'page' : undefined}
- >
- {Icon && (
-
- )}
- {item.label}
-
-
- )
- })}
-
-
-
- {docsUtilitySections.map((section) =>
- section.id === 'tools' ? (
-
- ) : (
-
- {section.label}
-
- ),
- )}
-
-
- )
-}
diff --git a/src/components/DocsSetupCards.styles.ts b/src/components/DocsSetupCards.styles.ts
index b4dbaf1c6..272c627e6 100644
--- a/src/components/DocsSetupCards.styles.ts
+++ b/src/components/DocsSetupCards.styles.ts
@@ -1,4 +1,3 @@
-import { inherited } from '../styles/inherited'
import { style } from '../styles/scoped'
import { vars as tokens } from '../styles/theme'
@@ -29,27 +28,27 @@ export const docsSetupCard = style({
backgroundColor: tokens.color.background,
color: tokens.color.foreground,
selectors: {
+ // Card title 16 medium, 8 from its icon and 4 above the 14/20 description.
'& h3': {
display: 'flex',
alignItems: 'center',
- gap: tokens.spacing['2_5'],
+ gap: tokens.spacing['2'],
margin: 0,
- fontSize: tokens.fontSize.lead,
+ fontSize: tokens.fontSize.body,
fontWeight: tokens.fontWeight.medium,
- lineHeight: tokens.lineHeight.snug,
+ lineHeight: tokens.lineHeight.body,
},
'& svg': {
flexShrink: 0,
},
'& p': {
- marginTop: tokens.spacing['3'],
+ marginTop: tokens.spacing['1'],
marginInlineEnd: tokens.spacing['0'],
marginBottom: tokens.spacing['6'],
marginInlineStart: tokens.spacing['0'],
-
- color: inherited.color.colorMixInSrgbColorForeground65Transparent,
+ color: tokens.color.muted,
fontSize: tokens.fontSize.sm,
- lineHeight: tokens.lineHeight.relaxed,
+ lineHeight: tokens.lineHeight.control,
},
},
})
@@ -75,8 +74,8 @@ export const docsSetupLink = style({
'--corner-radius': tokens.radius.md,
backgroundColor: tokens.color.container,
color: tokens.color.foreground,
- fontSize: tokens.fontSize.compact,
- fontWeight: tokens.fontWeight.medium,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
lineHeight: tokens.lineHeight.control,
textDecoration: 'none',
whiteSpace: 'nowrap',
diff --git a/src/components/DocsSidebarDrawer.recipes.ts b/src/components/DocsSidebarDrawer.recipes.ts
deleted file mode 100644
index 5e1b69d15..000000000
--- a/src/components/DocsSidebarDrawer.recipes.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { inherited } from '../styles/inherited'
-import { style } from '../styles/recipes'
-import { vars as tokens } from '../styles/theme'
-export const docsSidebarDrawerButton = style({
- display: 'flex',
- minHeight: tokens.spacing['9'],
- alignItems: 'center',
- gap: tokens.spacing['1_5'],
- borderRadius: tokens.radius.md,
- fontFamily: tokens.fontFamily.book,
-
- color: inherited.color.colorMixInOklabForeground70Transparent,
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
- selectors: {
- '&:hover': {
- '@media (hover: hover)': {
- color: tokens.color.foreground,
- },
- },
- '&:focus-visible': {
- outlineStyle: 'solid',
- outlineWidth: '2px',
- outlineOffset: '4px',
- },
- },
- '@media (width >= 1080px)': {
- display: 'none',
- },
-})
-export const docsSidebarDrawerLayout = style({
- position: 'fixed',
- inset: '0',
- zIndex: tokens.zIndex.drawer,
- selectors: { '&[aria-hidden="true"]': { pointerEvents: 'none' } },
- '@media (width >= 1080px)': {
- display: 'none',
- },
-})
-export const docsSidebarDrawerLayout3 = style({
- opacity: 1,
- selectors: { '[aria-hidden="true"] > &': { opacity: 0 } },
- position: 'absolute',
- inset: '0',
-
- backgroundColor: inherited.color.colorMixInOklabColorBlack40Transparent,
- transitionProperty: 'opacity',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '200ms',
- '@media (prefers-reduced-motion: reduce)': {
- transitionProperty: 'none',
- },
-})
-export const docsSidebarDrawerLayout6 = style({
- translate: '0',
- selectors: { '[aria-hidden="true"] > &': { translate: '-100% 0' } },
- position: 'absolute',
- top: '0',
- insetInlineStart: '0',
- display: 'flex',
- height: '100%',
- width: '82%',
- maxWidth: '320px',
- flexDirection: 'column',
- borderInlineEndStyle: 'solid',
- borderInlineEndWidth: tokens.borderWidth.hairline,
- borderColor: tokens.color.line,
- backgroundColor: tokens.color.background,
- transitionProperty: 'transform, translate, scale, rotate',
- transitionTimingFunction: 'var(--ease-out)',
- transitionDuration: '200ms',
- '@media (prefers-reduced-motion: reduce)': {
- transitionProperty: 'none',
- },
-})
-export const docsSidebarDrawerLayout9 = style({
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'space-between',
- borderBottomStyle: 'solid',
- borderBottomWidth: tokens.borderWidth.hairline,
- borderColor: tokens.color.line,
- paddingInline: tokens.spacing['5'],
- paddingBlock: tokens.spacing['4'],
-})
-export const docsSidebarDrawerText = style({
- fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.bodySmall,
- letterSpacing: tokens.letterSpacing.normal,
- color: tokens.color.foreground,
-})
-export const docsSidebarDrawerButton2 = style({
- display: 'grid',
- width: tokens.spacing['10'],
- height: tokens.spacing['10'],
- placeItems: 'center',
- borderRadius: tokens.radius.lg,
-
- color: inherited.color.colorMixInOklabForeground70Transparent,
- transitionProperty:
- 'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
- transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
- transitionDuration: '150ms',
- selectors: {
- '&:hover': {
- '@media (hover: hover)': {
- backgroundColor: inherited.color.colorMixInOklabForeground5Transparent,
- color: tokens.color.foreground,
- },
- },
- '&:focus-visible': {
- outlineStyle: 'solid',
- outlineWidth: '2px',
- outlineOffset: '2px',
- },
- },
-})
-export const docsSidebarDrawerLayout10 = style({
- display: 'flex',
- flex: '1 1 0%',
- flexDirection: 'column',
- overflowY: 'auto',
- paddingInline: tokens.spacing['5'],
- paddingBlock: tokens.spacing['3'],
-})
diff --git a/src/components/DocsSidebarDrawer.tsx b/src/components/DocsSidebarDrawer.tsx
deleted file mode 100644
index 7a335f106..000000000
--- a/src/components/DocsSidebarDrawer.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-'use client'
-
-import { useEffect, useId, useMemo, useRef, useState } from 'react'
-import { createPortal } from 'react-dom'
-import { useActiveSidebarAnchor, useConfig } from 'vocs'
-import { resolveSidebarItems, SidebarNodes, usePathname } from './DocsHeader'
-import * as ui from './DocsSidebarDrawer.recipes'
-
-function collectSidebarLinks(nodes: ReturnType): string[] {
- return nodes.flatMap((node) => [
- ...(node.link ? [node.link] : []),
- ...collectSidebarLinks(node.items ?? []),
- ])
-}
-
-/**
- * Mobile-only docs sidebar drawer.
- *
- * Injects a hamburger toggle into the sticky "On this page" outline row (Vocs'
- * `[data-v-outline-mobile]` bar) so the docs sidebar stays reachable while
- * scrolling, then slides the sidebar tree in from the left. Pages without an
- * outline get a standalone menu row in the same position.
- */
-export default function DocsSidebarDrawer() {
- const pathname = usePathname()
- const config = useConfig()
- const items = resolveSidebarItems(config?.sidebar, pathname)
- const links = useMemo(() => collectSidebarLinks(items), [items])
- const activeAnchor = useActiveSidebarAnchor(links)
- const [host, setHost] = useState(null)
- const [open, setOpen] = useState(false)
- const triggerRef = useRef(null)
- const panelRef = useRef(null)
- const dialogId = useId()
- const titleId = useId()
-
- // Mount a portal host inside the outline row, re-attaching whenever the route
- // changes (Vocs re-creates the row per page).
- useEffect(() => {
- if (pathname === '/') return
- let span: HTMLElement | null = null
-
- const attach = () => {
- const row = document.querySelector('[data-v-outline-mobile] > div')
- const main = document.querySelector('[data-layout] > [data-v-main]')
- const target = row ?? (main?.querySelector('.tempo-docs-home') ? null : main)
- if (span?.isConnected && span.parentElement === target) return
- span?.remove()
- span = null
- if (!target) {
- setHost(null)
- return
- }
- span = document.createElement('span')
- span.dataset.docsSidebarToggle = ''
- if (!row) span.dataset.docsSidebarFallback = ''
- target.prepend(span)
- setHost(span)
- }
-
- attach()
- // Keep observing: a client navigation can replace the row after its pathname
- // updates, including when moving between pages within the same docs layout.
- const observer = new MutationObserver(attach)
- observer.observe(document.body, { childList: true, subtree: true })
- return () => {
- observer.disconnect()
- span?.remove()
- setHost(null)
- }
- }, [pathname])
-
- // Close on route change.
- useEffect(() => {
- setOpen(false)
- }, [pathname])
-
- // Keep keyboard focus in the modal and return it to the trigger on dismissal.
- useEffect(() => {
- if (!open) return
- const panel = panelRef.current
- if (!panel) return
- const desktop = window.matchMedia('(min-width: 1080px)')
- const focusableElements = () =>
- Array.from(
- panel.querySelectorAll(
- 'a[href], button:not([disabled]), summary, input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
- ),
- ).filter(
- (element) =>
- element.tabIndex >= 0 &&
- // Closed descendants can retain client rects but cannot receive focus.
- element.checkVisibility({ visibilityProperty: true }) &&
- !element.closest('[inert]'),
- )
- const focusFirst = () => (focusableElements()[0] ?? panel).focus()
- const onFocus = (event: FocusEvent) => {
- if (event.target instanceof Node && !panel.contains(event.target)) focusFirst()
- }
- const onKey = (event: KeyboardEvent) => {
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
- // Release this modal before Vocs opens search and takes keyboard focus.
- event.preventDefault()
- event.stopImmediatePropagation()
- setOpen(false)
- requestAnimationFrame(() => {
- document.dispatchEvent(
- new KeyboardEvent('keydown', {
- key: 'k',
- code: 'KeyK',
- bubbles: true,
- cancelable: true,
- metaKey: event.metaKey,
- ctrlKey: event.ctrlKey,
- }),
- )
- })
- return
- }
- if (event.key === 'Escape') {
- event.preventDefault()
- event.stopImmediatePropagation()
- setOpen(false)
- }
- if (event.key !== 'Tab') return
- const elements = focusableElements()
- const first = elements[0]
- const last = elements.at(-1)
- if (!first || !last) {
- event.preventDefault()
- panel.focus()
- } else if (
- event.shiftKey &&
- (document.activeElement === first || document.activeElement === panel)
- ) {
- event.preventDefault()
- last.focus()
- } else if (
- !event.shiftKey &&
- (document.activeElement === last || document.activeElement === panel)
- ) {
- event.preventDefault()
- first.focus()
- }
- }
- const closeOnDesktop = () => {
- if (desktop.matches) setOpen(false)
- }
- focusFirst()
- closeOnDesktop()
- document.addEventListener('keydown', onKey, true)
- document.addEventListener('focusin', onFocus)
- desktop.addEventListener('change', closeOnDesktop)
- const prevOverflow = document.body.style.overflow
- document.body.style.overflow = 'hidden'
- return () => {
- document.removeEventListener('keydown', onKey, true)
- document.removeEventListener('focusin', onFocus)
- desktop.removeEventListener('change', closeOnDesktop)
- document.body.style.overflow = prevOverflow
- // Activation may not focus the trigger, and the outline can replace its
- // portal host. Restore the current button rather than a captured element.
- triggerRef.current?.focus({ preventScroll: true })
- }
- }, [open])
-
- if (pathname === '/' || items.length === 0) return null
-
- const toggle = host
- ? createPortal(
- setOpen((value) => !value)}
- aria-label={open ? 'Close docs navigation' : 'Open docs navigation'}
- aria-expanded={open}
- aria-controls={dialogId}
- aria-haspopup="dialog"
- {...ui.docsSidebarDrawerButton()}
- >
-
- Docs navigation
-
-
- Menu
- ,
- host,
- )
- : null
-
- return (
- <>
- {toggle}
-
- {/* Backdrop */}
- {/* biome-ignore lint/a11y/noStaticElementInteractions: backdrop dismiss. */}
- {/* biome-ignore lint/a11y/useKeyWithClickEvents: Escape handled globally. */}
-
setOpen(false)} {...ui.docsSidebarDrawerLayout3()} />
- {/* Panel */}
-
-
-
- Documentation
-
-
setOpen(false)}
- aria-label="Close docs navigation"
- {...ui.docsSidebarDrawerButton2()}
- >
-
- Close
-
-
-
-
-
- setOpen(false)}
- />
-
-
-
- >
- )
-}
diff --git a/src/components/IconButton.recipes.ts b/src/components/IconButton.recipes.ts
index f4b0f5117..2ed65d8bc 100644
--- a/src/components/IconButton.recipes.ts
+++ b/src/components/IconButton.recipes.ts
@@ -1,8 +1,8 @@
import { inherited } from '../styles/inherited'
import { style, vars as tokens } from '../styles/theme'
-// TDS Platform IconButton, secondary, small scale (tempoxyz/ds@3fd4cf7), with the
-// header controls' smoothed lg radius (G8) instead of the DS circle.
+// TDS Platform IconButton, secondary, small scale (tempoxyz/ds@3fd4cf7): 32 x 32
+// at radius 8, like every 32-high control.
export const iconButton = style({
display: 'inline-flex',
alignItems: 'center',
@@ -13,8 +13,8 @@ export const iconButton = style({
height: '32px',
padding: tokens.spacing['0'],
border: 0,
- borderRadius: tokens.radius.lg,
- '--corner-radius': tokens.radius.lg,
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
backgroundColor: tokens.color.container,
color: inherited.color.vocsTextColorPrimary,
cursor: 'pointer',
diff --git a/src/components/IndexSupplyQuery.recipes.ts b/src/components/IndexSupplyQuery.recipes.ts
index c3748118a..f54da99e4 100644
--- a/src/components/IndexSupplyQuery.recipes.ts
+++ b/src/components/IndexSupplyQuery.recipes.ts
@@ -2,7 +2,7 @@ import { inherited } from '../styles/inherited'
import { style } from '../styles/recipes'
import { vars as tokens } from '../styles/theme'
export const renderCellValueText = style({
- color: tokens.color.gray9,
+ color: tokens.color.muted,
fontStyle: 'italic',
})
export const renderCellValueLink = style({
@@ -48,11 +48,12 @@ export const indexSupplyQueryLayout3 = style({
display: 'flex',
alignItems: 'center',
gap: tokens.spacing['1_5'],
- fontSize: tokens.fontSize.compact,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
color: tokens.color.gray11,
})
export const indexSupplyQueryLink = style({
- color: tokens.color.gray9,
+ color: tokens.color.muted,
transitionProperty:
'color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tempo-style-gradient-from, --tempo-style-gradient-via, --tempo-style-gradient-to',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
@@ -88,7 +89,8 @@ export const indexSupplyQueryLayout5 = style({
paddingBlock: tokens.spacing['1'],
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
})
export const indexSupplyQueryText = style({
width: tokens.spacing['2'],
@@ -108,7 +110,7 @@ export const indexSupplyQueryText3 = style({
})
export const sqlEditor = style({
width: '100%',
- borderRadius: tokens.radius.smRem,
+ borderRadius: tokens.radius.lg,
borderStyle: 'solid',
borderWidth: tokens.borderWidth.hairline,
borderColor: tokens.color.gray4,
@@ -126,17 +128,15 @@ export const sqlEditor = style({
},
})
export const sqlEditor2 = style({
- fontSize: tokens.fontSize.caption,
- lineHeight: tokens.lineHeight.snug,
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
})
export const sqlEditor3 = style({
- fontSize: tokens.fontSize.compact,
-
+ fontSize: tokens.fontSize.xs,
lineHeight: inherited.lineHeight.leadingNormal,
})
export const indexSupplyQueryLayout6 = style({
- borderRadius: tokens.radius.smRem,
-
+ borderRadius: tokens.radius.lg,
backgroundColor: inherited.color.backgroundColorDestructiveTint,
paddingInline: tokens.spacing['3'],
paddingBlock: tokens.spacing['2'],
@@ -150,7 +150,7 @@ export const indexSupplyQueryLayout6 = style({
})
export const indexSupplyQueryLayout7 = style({
overflow: 'auto',
- borderRadius: tokens.radius.smRem,
+ borderRadius: tokens.radius.lg,
borderStyle: 'solid',
borderWidth: tokens.borderWidth.hairline,
borderColor: tokens.color.gray4,
@@ -180,7 +180,7 @@ export const indexSupplyQueryLayout8 = style({
export const td = style({
paddingBlock: tokens.spacing['4'],
textAlign: 'center',
- color: tokens.color.gray9,
+ color: tokens.color.muted,
})
export const tr = style({
borderBottomStyle: 'solid',
diff --git a/src/components/RelatedDocsLinks.recipes.ts b/src/components/RelatedDocsLinks.recipes.ts
index 63880965c..87168e1a6 100644
--- a/src/components/RelatedDocsLinks.recipes.ts
+++ b/src/components/RelatedDocsLinks.recipes.ts
@@ -1,4 +1,3 @@
-import { inherited } from '../styles/inherited'
import { style } from '../styles/recipes'
import { vars as tokens } from '../styles/theme'
export const relatedDocsLinksSection = style({
@@ -11,15 +10,16 @@ export const relatedDocsLinksSection = style({
export const relatedDocsLinksHeading = style({
marginBottom: tokens.spacing['4'],
fontFamily: tokens.fontFamily.book,
- fontSize: tokens.fontSize.subheading,
- fontWeight: tokens.fontWeight.medium,
+ fontSize: tokens.fontSize.title,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.heading,
letterSpacing: tokens.letterSpacing.tight,
color: tokens.color.foreground,
})
export const relatedDocsLinksList = style({
display: 'grid',
listStyleType: 'none',
- gap: tokens.spacing['3'],
+ gap: tokens.spacing['4'],
padding: tokens.spacing['0'],
'@media (width >= 40rem)': {
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
@@ -36,7 +36,7 @@ export const link = style({
borderStyle: 'solid',
borderWidth: tokens.borderWidth.hairline,
borderColor: tokens.color.line,
- padding: tokens.spacing['4'],
+ padding: tokens.spacing['5'],
color: tokens.color.foreground,
textDecorationLine: 'none',
transitionProperty:
@@ -46,15 +46,14 @@ export const link = style({
})
export const relatedDocsLinksText = style({
display: 'block',
- fontSize: tokens.fontSize.bodySmall,
- lineHeight: tokens.spacing['5'],
+ fontSize: tokens.fontSize.body,
+ lineHeight: tokens.lineHeight.body,
fontWeight: tokens.fontWeight.medium,
})
export const relatedDocsLinksText2 = style({
- marginTop: tokens.spacing['1_5'],
+ marginTop: tokens.spacing['1'],
display: 'block',
fontSize: tokens.fontSize.sm,
- lineHeight: tokens.spacing['5'],
-
- color: inherited.color.colorMixInOklabForeground60Transparent,
+ lineHeight: tokens.lineHeight.control,
+ color: tokens.color.muted,
})
diff --git a/src/components/RouterLink.tsx b/src/components/RouterLink.tsx
new file mode 100644
index 000000000..826d161c3
--- /dev/null
+++ b/src/components/RouterLink.tsx
@@ -0,0 +1,9 @@
+import type { ComponentProps, ReactNode } from 'react'
+import { Link } from 'waku'
+
+type Props = Omit
, 'children'> & { children?: ReactNode }
+
+/** A waku Link for a DS part's `render` prop, which passes the children in. */
+export function RouterLink({ children, ...props }: Props) {
+ return {children}
+}
diff --git a/src/components/RoutesTester.tsx b/src/components/RoutesTester.tsx
index f23956aaa..1bc961d2c 100644
--- a/src/components/RoutesTester.tsx
+++ b/src/components/RoutesTester.tsx
@@ -2068,7 +2068,7 @@ const referenceLink = style({
})
const copiedNote = style({
marginInlineStart: tokens.spacing['2'],
- fontFamily: tokens.fontFamily.book,
+ fontFamily: tokens.fontFamily.ui,
color: tokens.color.gray10,
})
const quoteSummary = style({
diff --git a/src/components/SegmentedControl.recipes.ts b/src/components/SegmentedControl.recipes.ts
index 12fa17fc3..bcb37aa96 100644
--- a/src/components/SegmentedControl.recipes.ts
+++ b/src/components/SegmentedControl.recipes.ts
@@ -1,16 +1,21 @@
-import { inherited } from '../styles/inherited'
import { style, vars as tokens } from '../styles/theme'
-// TDS Platform SegmentedControl (tempoxyz/ds@3fd4cf7), ported by hand. The DS
-// swaps the selected fill instantly; here the fill fades with the shared motion.
+// DS Web SegmentedControl geometry (tempoxyz/ds@3fd4cf7), ported by hand. The selected
+// item takes the docs' one selected look, the container chip with primary medium text,
+// on a page-colored track inside a hairline; the fill fades with the shared motion.
export const control = style({
display: 'flex',
boxSizing: 'border-box',
maxWidth: '100%',
padding: tokens.spacing['1'],
- borderRadius: tokens.radius.lg,
- '--corner-radius': tokens.radius.lg,
- backgroundColor: tokens.color.container,
+ // A 32-high track at radius 8; items inset 4 take radius 4 (8 - 4).
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ // An inset outline draws the hairline without adding to the 32 height.
+ outlineWidth: tokens.borderWidth.hairline,
+ outlineStyle: 'solid',
+ outlineColor: tokens.color.hairline,
+ outlineOffset: '-1px',
overflowX: 'auto',
overscrollBehaviorX: 'contain',
scrollbarWidth: 'none',
@@ -41,17 +46,17 @@ export const item = style({
gap: tokens.spacing['1_5'],
boxSizing: 'border-box',
minWidth: '80px',
- height: '40px',
- paddingBlock: tokens.spacing['1'],
- paddingInline: tokens.spacing['4'],
+ height: '24px',
+ paddingBlock: tokens.spacing['0'],
+ paddingInline: tokens.spacing['3'],
border: 0,
- borderRadius: tokens.radius.md,
- '--corner-radius': tokens.radius.md,
+ borderRadius: tokens.radius.sm,
+ '--corner-radius': tokens.radius.sm,
backgroundColor: 'transparent !custom',
- color: inherited.color.vocsTextColorPrimary,
+ color: tokens.color.muted,
fontFamily: 'inherit !custom',
- fontSize: tokens.fontSize.xs,
- lineHeight: tokens.lineHeight.caption,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
whiteSpace: 'nowrap',
cursor: 'pointer',
transitionProperty: 'color, background-color, border-color, opacity, transform',
@@ -59,7 +64,9 @@ export const item = style({
transitionTimingFunction: 'var(--tempo-ease)',
selectors: {
'&[data-checked]': {
- backgroundColor: tokens.color.segmentSelected,
+ color: tokens.color.foreground,
+ fontWeight: tokens.fontWeight.medium,
+ backgroundColor: tokens.color.container,
transitionDuration: 'var(--tempo-enter)',
},
'&:focus-visible': {
@@ -71,4 +78,12 @@ export const item = style({
'&:disabled': { cursor: 'default' },
'& > svg': { flexShrink: 0, width: '14px', height: '14px' },
},
+ '@media (hover: hover)': {
+ selectors: {
+ '&:not([data-checked]):not(:disabled):hover': {
+ backgroundColor: tokens.color.containerSubtle,
+ transitionDuration: 'var(--tempo-enter)',
+ },
+ },
+ },
})
diff --git a/src/components/SignatureSelector.recipes.ts b/src/components/SignatureSelector.recipes.ts
index 55cf8041d..049418ebb 100644
--- a/src/components/SignatureSelector.recipes.ts
+++ b/src/components/SignatureSelector.recipes.ts
@@ -17,7 +17,7 @@ export const signatureSelectorLayout2 = style({
})
export const label = style({
display: 'block',
- fontSize: tokens.fontSize.compact,
+ fontSize: tokens.fontSize.sm,
color: tokens.color.gray11,
})
export const signatureSelectorInput = style({
@@ -29,7 +29,7 @@ export const signatureSelectorInput = style({
borderWidth: tokens.borderWidth.hairline,
borderColor: tokens.color.gray4,
paddingInline: tokens.spacing['3'],
- fontSize: tokens.fontSize.compact,
+ fontSize: tokens.fontSize.sm,
selectors: {
'&:focus': {
'--tempo-style-ring-shadow':
@@ -52,8 +52,8 @@ export const signatureSelectorButton = style({
insetInlineEnd: tokens.spacing['2'],
'--tempo-style-translate-y': 'calc(calc(1 / 2 * 100%) * -1)',
translate: 'var(--tempo-style-translate-x) var(--tempo-style-translate-y)',
- fontSize: tokens.fontSize.caption,
- color: tokens.color.gray9,
+ fontSize: tokens.fontSize.xs,
+ color: tokens.color.muted,
selectors: {
'&:hover': {
'@media (hover: hover)': {
@@ -85,8 +85,8 @@ export const signatureSelectorLayout4 = style({
paddingInline: tokens.spacing['3'],
paddingBlock: tokens.spacing['4'],
textAlign: 'center',
- fontSize: tokens.fontSize.compact,
- color: tokens.color.gray9,
+ fontSize: tokens.fontSize.sm,
+ color: tokens.color.muted,
})
export const signatureSelectorLayout5 = style({
borderBottomStyle: 'solid',
@@ -107,11 +107,11 @@ export const signatureSelectorLayout6 = style({
paddingInline: tokens.spacing['3'],
paddingBlock: tokens.spacing['1'],
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
fontWeight: tokens.fontWeight.medium,
letterSpacing: inherited.letterSpacing.trackingWide,
- color: tokens.color.gray10,
+ color: tokens.color.muted,
textTransform: 'uppercase',
})
export const signatureSelectorLayout7 = style({
@@ -139,7 +139,7 @@ export const signatureSelectorInput2 = style({
export const signatureSelectorText = style({
flexShrink: 0,
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
color: tokens.color.gray12,
})
export const signatureSelectorText2 = style({
@@ -148,8 +148,8 @@ export const signatureSelectorText2 = style({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.caption,
- color: tokens.color.gray9,
+ fontSize: tokens.fontSize.xs,
+ color: tokens.color.muted,
})
export const signatureSelectorText3 = style({
marginInlineStart: 'auto !custom',
@@ -191,7 +191,7 @@ export const signatureSelectorLayout9 = style({
marginBlockEnd: tokens.spacing['2'],
},
},
- borderRadius: tokens.radius.smRem,
+ borderRadius: tokens.radius.lg,
borderStyle: 'solid',
borderWidth: tokens.borderWidth.hairline,
borderColor: tokens.color.gray4,
@@ -216,12 +216,12 @@ export const code = style({
paddingBlock: tokens.spacing['1'],
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
color: tokens.color.gray11,
})
export const signatureSelectorLayout12 = style({
- fontSize: tokens.fontSize.caption,
- color: tokens.color.gray10,
+ fontSize: tokens.fontSize.xs,
+ color: tokens.color.muted,
})
export const signatureSelectorLink = style({
color: inherited.color.textColorAccent,
@@ -252,7 +252,7 @@ export const signatureSelectorLayout14 = style({
paddingBlock: tokens.spacing['1'],
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
})
export const signatureSelectorText5 = style({
width: tokens.spacing['2'],
@@ -272,7 +272,7 @@ export const signatureSelectorText7 = style({
})
export const signatureSelectorButton3 = style({
lineHeight: tokens.lineHeight.none,
- color: tokens.color.gray9,
+ color: tokens.color.muted,
selectors: {
'&:hover': {
'@media (hover: hover)': {
@@ -285,7 +285,7 @@ export const signatureSelectorLayout15 = style({
borderColor: tokens.color.amber6,
backgroundColor: tokens.color.amber3,
color: tokens.color.amber11,
- borderRadius: tokens.radius.smRem,
+ borderRadius: tokens.radius.lg,
borderStyle: 'solid',
borderWidth: tokens.borderWidth.hairline,
paddingInline: tokens.spacing['3'],
@@ -304,7 +304,7 @@ export const signatureSelectorLayout16 = style({
marginBlockEnd: tokens.spacing['2'],
},
},
- borderRadius: tokens.radius.smRem,
+ borderRadius: tokens.radius.lg,
borderStyle: 'solid',
borderWidth: tokens.borderWidth.hairline,
borderColor: tokens.color.blue4,
@@ -312,7 +312,7 @@ export const signatureSelectorLayout16 = style({
padding: tokens.spacing['3'],
})
export const signatureSelectorLayout17 = style({
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
fontWeight: tokens.fontWeight.medium,
color: tokens.color.blue11,
})
@@ -323,11 +323,11 @@ export const code2 = style({
paddingBlock: tokens.spacing['1'],
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
color: tokens.color.blue11,
})
export const signatureSelectorLayout18 = style({
- fontSize: tokens.fontSize.caption,
+ fontSize: tokens.fontSize.xs,
lineHeight: inherited.lineHeight.leadingRelaxed,
color: tokens.color.blue9,
diff --git a/src/components/SqlEditor.recipes.ts b/src/components/SqlEditor.recipes.ts
index 17691fb20..048521612 100644
--- a/src/components/SqlEditor.recipes.ts
+++ b/src/components/SqlEditor.recipes.ts
@@ -16,7 +16,8 @@ export const sqlEditorLayout2 = style({
padding: tokens.spacing['3'],
backgroundColor: tokens.color.gray2,
fontFamily: tokens.fontFamily.code,
- fontSize: tokens.fontSize.compact,
+ // Matches the 12px Monaco editor that replaces this preview once it loads.
+ fontSize: tokens.fontSize.xs,
color: tokens.color.gray11,
})
export const sqlEditorLayoutAppearance = instanceStyle(
diff --git a/src/lib/ai-docs.test.ts b/src/lib/ai-docs.test.ts
index 94f465767..2cc68babc 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(/## API reference[\s\S]*\[API reference\]/)
+ expect(output).toMatch(/## Network[\s\S]*\[Keys\][\s\S]*\[Node releases\][\s\S]*## API\n/)
+ expect(output).not.toMatch(/^## (Tempo EVM|Operate)$/m)
+ expect(output).toMatch(/## API\n[\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: [Network](/docs/development.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..bc9366deb 100644
--- a/src/lib/ai-docs.ts
+++ b/src/lib/ai-docs.ts
@@ -7,11 +7,11 @@ import {
} from './docs-sections'
export const aiDocsDescription =
- 'Build stablecoin products with Accounts, Earn, Routes, Zones, and Machine Payments, or build on Tempo EVM. Find API references, SDKs, CLI tools, and partner integrations.'
+ 'Build stablecoin products with Accounts, Earn, Routes, Zones, and Machine Payments, or build on the Tempo network. Find API references, SDKs, CLI tools, and partner integrations.'
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, find network details, 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,12 @@ 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, and protocol specifications. Run RPC, standby, and validator nodes, 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.',
}
@@ -80,7 +84,7 @@ export function renderAiIndex(content: string) {
'',
aiDocsDescription,
'',
- 'Start with the product guide for your task, then use the API reference, SDKs, and CLI for exact methods and schemas. Use Tempo EVM for chain behavior and protocol details.',
+ 'Start with the product guide for your task, then use the API reference, SDKs, and CLI for exact methods and schemas. Use Network for chain behavior, protocol details, and node operation.',
'',
aiDocsNotice(),
'[Docs skill](/SKILL.md) · [Full documentation](/llms-full.txt)',
diff --git a/src/lib/blog-migration.test.ts b/src/lib/blog-migration.test.ts
index 62d23ae8c..0795192d6 100644
--- a/src/lib/blog-migration.test.ts
+++ b/src/lib/blog-migration.test.ts
@@ -104,7 +104,7 @@ describe('engineering blog retirement', () => {
expect(JSON.stringify(vocsConfig.topNav)).not.toMatch(/blog/i)
for (const file of [
'src/components/DocsHeader.tsx',
- 'src/components/DocsSectionNav.tsx',
+ 'src/components/DocsMenuDrawer.tsx',
'src/marketing/app/_components/Header.tsx',
'src/marketing/app/_components/Footer.tsx',
'src/marketing/app/_components/HomeBelowFold.tsx',
diff --git a/src/lib/docs-search.test.ts b/src/lib/docs-search.test.ts
index 1bd869f0a..aecf04222 100644
--- a/src/lib/docs-search.test.ts
+++ b/src/lib/docs-search.test.ts
@@ -1,6 +1,7 @@
import MiniSearch from 'minisearch'
import { describe, expect, it } from 'vitest'
import config from '../../vocs.config'
+import { docsSearch } from './docs-search'
describe('documentation search', () => {
it('finds account pages while the user is still typing', () => {
@@ -37,4 +38,74 @@ describe('documentation search', () => {
'routes',
)
})
+
+ it('ranks guides above API operations for intent queries', () => {
+ // Vocs sends search functions to the browser as source text, so rebuild them from it.
+ const fromSource = (options: T): T =>
+ Object.fromEntries(
+ Object.entries(options as Record).map(([key, value]) => [
+ key,
+ typeof value === 'function' ? new Function(`return ${value.toString()}`)() : value,
+ ]),
+ ) as T
+ const indexOptions = fromSource(docsSearch.index)
+ const index = new MiniSearch({
+ ...indexOptions,
+ storeFields: ['category', 'href', 'searchPriority', 'title', 'titles', 'type'],
+ })
+ const operation = (group: string, id: string, title: string) => ({
+ id: `openapi:/docs/api/${group.toLowerCase().replaceAll(' ', '-')}#${id}`,
+ href: `/docs/api/${group.toLowerCase().replaceAll(' ', '-')}#${id}`,
+ category: 'Tempo API',
+ subtitle: '',
+ text: title,
+ title,
+ titles: ['Tempo API', group],
+ type: 'section',
+ })
+ const page = (href: string, title: string, text = '') => ({
+ id: href,
+ href,
+ category: 'Docs',
+ subtitle: '',
+ text,
+ title,
+ titles: [],
+ type: 'page',
+ })
+ index.addAll([
+ page(
+ '/docs/api/console/api-keys#how-to-create-and-manage-api-keys',
+ 'How to create and manage API keys',
+ 'API keys authenticate an integration and attribute its usage to a project.',
+ ),
+ operation('API keys', 'revokeapikey', 'Revoke API key'),
+ operation('API keys', 'rotateapikey', 'Rotate API key'),
+ page('/docs/quickstart/faucet#tempo-faucet', 'Tempo faucet: get testnet funds'),
+ { ...operation('Faucet', 'fundtestnetaccount', 'Fund testnet account') },
+ {
+ ...page('/docs/api/faucet', 'Faucet'),
+ id: 'openapi:/docs/api/faucet',
+ titles: ['Tempo API'],
+ },
+ page(
+ '/docs/quickstart/connection-details#network-details',
+ 'Network details',
+ 'Use Tempo Mainnet for production and Moderato testnet for development.',
+ ),
+ operation('JSON-RPC', 'eth_chainid', 'eth_chainId'),
+ { ...operation('JSON-RPC', 'eth_blocknumber', 'eth_blockNumber'), text: 'RPC url' },
+ ])
+ const queryOptions = { ...fromSource(docsSearch.query), tokenize: indexOptions.tokenize }
+ const search = (query: string) => index.search(query, queryOptions)
+
+ expect(search('api key')[0]?.title).toBe('How to create and manage API keys')
+ expect(search('faucet')[0]?.title).toBe('Tempo faucet: get testnet funds')
+ expect(search('chain id')[0]?.title).toBe('Network details')
+ expect(search('rpc url')[0]?.title).toBe('Network details')
+ // Operations still surface for their own names, under a breadcrumb that names the
+ // section once.
+ const revoke = search('revoke api key').find((result) => result.title === 'Revoke API key')
+ expect(revoke?.titles).toEqual(['API keys'])
+ })
})
diff --git a/src/lib/docs-search.ts b/src/lib/docs-search.ts
index a591e0a34..5f120ba3d 100644
--- a/src/lib/docs-search.ts
+++ b/src/lib/docs-search.ts
@@ -1,2 +1,159 @@
+import type { Options, SearchOptions } from 'minisearch'
+
// The website links here with ?search=1 to open Vocs search on arrival.
export const DOCS_SEARCH_PARAM = 'search'
+
+type SearchDocument = Record
+
+// Vocs sends these functions to the browser as source text, so each one stays
+// self-contained: no imports, module constants, or helpers outside its own body.
+
+function extractDocsSearchField(document: SearchDocument, fieldName: string) {
+ const href = String(document.href ?? '')
+ if (fieldName === 'path') {
+ return href
+ .split('#')[0]
+ .replace(/^\/docs\//, '/')
+ .replaceAll('/', ' ')
+ }
+ if (fieldName === 'excerpt') {
+ return String(document.text ?? '')
+ .trim()
+ .split(/\s+/)
+ .slice(0, 24)
+ .join(' ')
+ }
+ if (fieldName === 'aliases' || fieldName === 'keywords') {
+ // Terms people search for that these pages' headings don't use. Only the page
+ // document gets them, so it outranks its own sections. Two-word phrases go to
+ // `aliases`; single words go to `keywords`, which carries less weight.
+ if (document.type !== 'page') return undefined
+ const aliases: Record = {
+ '/docs/api/console/api-keys': ['api key', 'api keys', 'api token'],
+ '/docs/api/json-rpc': ['json rpc'],
+ '/docs/quickstart/connection-details': [
+ 'rpc',
+ 'rpc url',
+ 'rpc endpoint',
+ 'chain id',
+ 'explorer',
+ ],
+ '/docs/quickstart/faucet': [
+ 'faucet',
+ 'testnet faucet',
+ 'test tokens',
+ 'testnet tokens',
+ 'testnet funds',
+ ],
+ '/get-started/quickstart': ['first payment'],
+ }
+ const terms = (aliases[href.split('#')[0]] ?? []).filter(
+ (alias) => alias.includes(' ') === (fieldName === 'aliases'),
+ )
+ return terms.length ? terms.join('|') : undefined
+ }
+ if (fieldName === 'titles' && Array.isArray(document.titles)) {
+ // Results show `category › ...titles`. Drop a title that repeats the category or
+ // the title before it, so an endpoint reads "Tempo API › API keys".
+ let previous = String(document.category ?? '')
+ .trim()
+ .toLowerCase()
+ const titles: string[] = []
+ for (const title of document.titles) {
+ const current = String(title).trim().toLowerCase()
+ if (current === previous) continue
+ titles.push(String(title))
+ previous = current
+ }
+ return titles
+ }
+ return document[fieldName]
+}
+
+function stringifyDocsSearchField(fieldValue: unknown) {
+ // Join title stacks with spaces; the default comma join fuses the words around it.
+ return Array.isArray(fieldValue) ? fieldValue.join(' ') : String(fieldValue)
+}
+
+function tokenizeDocsSearch(text: string, fieldName?: string) {
+ // Each alias phrase is one term ("chain id" → ":chain-id"), so its words don't
+ // boost the page for other queries such as "project id". The leading colon, which
+ // the word split below removes from every other term, keeps a single word such as
+ // "rpc" from prefix-matching a phrase.
+ if (fieldName === 'aliases') {
+ return text.split('|').map((alias) => `:${alias.trim().toLowerCase().replace(/\s+/g, '-')}`)
+ }
+ if (fieldName === 'keywords') return text.split('|')
+ // The Vocs tokenizer: whitespace, punctuation, paths, and camelCase.
+ const words = text.split(/[\s\-._/:@]+/).filter(Boolean)
+ const tokens: string[] = []
+ for (const word of words) {
+ tokens.push(word.toLowerCase())
+ const split = word
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
+ .split(/\s+/)
+ .map((part) => part.toLowerCase())
+ .filter(Boolean)
+ if (split.length > 1) tokens.push(...split)
+ }
+ // Queries (no field name) also search each pair of adjacent words as an alias term.
+ if (fieldName === undefined) {
+ for (let index = 1; index < words.length; index++) {
+ tokens.push(`:${words[index - 1]}-${words[index]}`.toLowerCase())
+ }
+ }
+ return tokens
+}
+
+function fuzzyDocsSearch(term: string) {
+ // Alias phrase terms match exactly or by prefix, never fuzzily.
+ return term.startsWith(':') ? false : 0.1
+}
+
+function boostDocsSearchDocument(
+ documentId: unknown,
+ _term: string,
+ storedFields?: Record,
+) {
+ const priority = (storedFields?.searchPriority as number | undefined) ?? 1
+ const href = (storedFields?.href as string | undefined) ?? ''
+ const segments = href ? href.split('/').filter(Boolean).length : 1
+ const depth = href.startsWith('/docs/') ? Math.max(segments - 1, 1) : segments
+ const docsBoost = href.startsWith('/docs/') ? 1.5 : 1
+ // Guides answer intent queries ("api key", "faucet"), so generated API reference
+ // entries rank below them and still surface for their own names. Home sections
+ // repeat the pages they link to.
+ let kindBoost = 1
+ if (String(documentId).startsWith('openapi:')) {
+ kindBoost = storedFields?.type === 'section' ? 0.5 : 0.6
+ } else if (href === '/' || href.startsWith('/#')) {
+ kindBoost = 0.35
+ } else if (
+ storedFields?.type === 'page' &&
+ /^\/(?:get-started|docs\/guide|docs\/quickstart|docs\/api)\//.test(href)
+ ) {
+ kindBoost = 1.25
+ }
+ return priority * (1 / Math.max(depth, 1)) * docsBoost * kindBoost
+}
+
+export const docsSearch = {
+ index: {
+ fields: ['title', 'titles', 'subtitle', 'path', 'excerpt', 'aliases', 'keywords'],
+ storeFields: ['path', 'excerpt'],
+ extractField: extractDocsSearchField,
+ stringifyField: stringifyDocsSearchField,
+ tokenize: tokenizeDocsSearch,
+ },
+ query: {
+ combineWith: 'OR',
+ fuzzy: fuzzyDocsSearch,
+ prefix: true,
+ boost: { title: 5, subtitle: 3, titles: 2, path: 3, excerpt: 3, aliases: 60, keywords: 20 },
+ boostDocument: boostDocsSearchDocument,
+ },
+} satisfies {
+ index: Pick
+ query: SearchOptions
+}
diff --git a/src/lib/docs-sections.ts b/src/lib/docs-sections.ts
index 3419da23a..35e5990fc 100644
--- a/src/lib/docs-sections.ts
+++ b/src/lib/docs-sections.ts
@@ -9,6 +9,7 @@ export type DocsSection = {
| 'machine-payments'
| 'api'
| 'tools'
+ | 'resources'
| 'ecosystem'
label: string
href: string
@@ -19,19 +20,15 @@ export type DocsSection = {
export const docsSections: DocsSection[] = [
{
id: 'overview',
- label: 'Get Started',
+ label: 'Get started',
href: '/get-started',
matches: [
'/get-started',
- '/docs/resources',
'/docs/quickstart/faucet',
+ '/docs/quickstart/connection-details',
'/docs/guide/using-tempo-with-ai',
- '/docs/tools',
'/docs/developer-tools',
'/docs/quickstart/developer-tools',
- '/docs/sdk',
- '/docs/cli',
- '/docs/wallet',
],
},
{
@@ -54,9 +51,11 @@ export const docsSections: DocsSection[] = [
href: '/docs/agents',
matches: ['/docs/agents', '/docs/guide/machine-payments', '/docs/guide/mercator'],
},
+ // Network merges Tempo EVM (building on the chain) and Operate (nodes, upgrades and releases).
+ // It keeps the `developers` id that sidebars, AI exports and tests key on.
{
id: 'developers',
- label: 'Tempo EVM',
+ label: 'Network',
href: '/docs/development',
matches: [
'/docs/accounts/keys',
@@ -68,11 +67,11 @@ 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',
+ '/docs/changelog',
],
},
]
@@ -80,10 +79,22 @@ export const docsSections: DocsSection[] = [
export const docsUtilitySections: DocsSection[] = [
{
id: 'api',
- label: 'API reference',
+ label: 'API',
href: '/docs/api',
matches: ['/docs/api'],
},
+ {
+ id: 'tools',
+ label: 'SDKs & CLI',
+ href: '/docs/sdk',
+ matches: ['/docs/tools', '/docs/sdk', '/docs/cli', '/docs/wallet'],
+ },
+ {
+ id: 'resources',
+ label: 'Guides',
+ href: '/docs/resources/guides',
+ matches: ['/docs/resources'],
+ },
{
id: 'ecosystem',
label: 'Partners',
@@ -98,6 +109,13 @@ export const docsUtilitySections: DocsSection[] = [
},
]
+/**
+ * The Resources menu in the top bar, in menu order: the only menu there. The products
+ * (`docsSections`) are plain links; the API reference has no top-bar entry, since each
+ * product's sidebar ends with its own API reference group.
+ */
+export const docsResourceSectionIds: DocsSection['id'][] = ['resources', 'ecosystem', 'tools']
+
export function normalizeDocsSectionPath(path: string) {
return path.replace(/^\/developers(?=\/|$)/, '').replace(/\/+$/, '') || '/'
}
@@ -113,3 +131,11 @@ 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 const docsResourceSections = docsResourceSectionIds
+ .map((id) => getDocsSectionById(id))
+ .filter((section): section is DocsSection => Boolean(section))
diff --git a/src/lib/markdown-output.ts b/src/lib/markdown-output.ts
index 229d91f6e..a0494f1c8 100644
--- a/src/lib/markdown-output.ts
+++ b/src/lib/markdown-output.ts
@@ -595,7 +595,7 @@ function renderCallout(
function renderLinkButton(node: MarkdownNode): MarkdownNode[] {
const destination = requiredStringAttribute(node, 'href')
- // Home guide buttons read "Get started"; the guide's name makes the readable link.
+ // Home guide buttons carry a short action; the guide's name makes the readable link.
const label = optionalStaticStringAttribute(node, 'guide') ?? plainText(node.children ?? [])
if (!label) throw new TypeError(`${node.name} requires text content for Markdown output.`)
const content = link(label, destination)
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..5ddbf2686 100644
--- a/src/lib/og-sections.ts
+++ b/src/lib/og-sections.ts
@@ -22,7 +22,7 @@ export const ogSectionMap: Record = {
build: 'BUILD',
cli: 'CLI',
'developer-tools': 'DEVELOPER TOOLS',
- development: 'TEMPO EVM',
+ development: 'NETWORK',
earn: 'EARN',
ecosystem: 'ECOSYSTEM',
'get-started': 'GET STARTED',
@@ -41,6 +41,12 @@ export const ogSectionMap: Record = {
zones: 'ZONES',
}
+/** Route prefix → section label, checked before `ogSectionMap`. Network's node and upgrade pages live under `guide` and `protocol`. */
+export const ogSectionPrefixMap: Record = {
+ '/guide/node': 'NETWORK',
+ '/protocol/upgrades': 'NETWORK',
+}
+
/** 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..90f8a0c45 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,95 @@ 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, then its API reference', () => {
+ type SidebarNode = { text?: string; items?: SidebarNode[] }
+ const sidebar = sidebarFor('/get-started') as SidebarNode[]
+ expect(sidebar.map(({ text }) => text)).toEqual(['Get Started', 'API reference'])
+ expect(links(sidebar)).toEqual(
+ expect.arrayContaining([
+ '/docs/quickstart/faucet',
+ '/docs/quickstart/connection-details',
+ '/docs/guide/using-tempo-with-ai',
+ ]),
+ )
+ expect(sidebarFor('/docs/quickstart/connection-details')).toBe(sidebar)
+ })
+
+ it('merges Tempo EVM and Operate into Network: chain groups, then Run a node, then Changelog', () => {
+ type SidebarNode = { text?: string; link?: string; items?: SidebarNode[] }
+ const sidebar = sidebarFor('/docs/development') as SidebarNode[]
+ expect(sidebar.map(({ text }) => text)).toEqual([
+ 'Tempo EVM',
+ 'Run a node',
+ 'Changelog',
+ 'API reference',
+ ])
+ expect(sidebarFor('/docs/guide/node')).toBe(sidebar)
+ expect(sidebarFor('/docs/changelog')).toBe(sidebar)
+ expect(sidebarFor('/docs/protocol/upgrades/t13')).toBe(sidebar)
+ expect(sidebar[2].items?.map(({ text }) => text)).toEqual([
+ 'Overview',
+ 'Network upgrades',
+ 'Node releases',
+ 'Operator updates',
+ ])
+ })
+
+ it.each([
+ ['/docs/accounts', 'https://wallet.tempo.xyz'],
+ ['/docs/agents', 'https://mpp.dev'],
+ ['/docs/development', 'https://explore.tempo.xyz'],
+ ])('%s sidebar links %s, which left the header', (landing, href) => {
+ expect(links(sidebarFor(landing))).toContain(href)
+ })
+
+ it.each([
+ ['/docs/guide/node/validator-keys', 'developers', '/docs/development'],
+ ['/docs/guide/node/network-upgrades', 'developers', '/docs/development'],
+ ['/docs/protocol/upgrades/t11', 'developers', '/docs/development'],
+ ['/docs/changelog', 'developers', '/docs/development'],
+ ['/docs/cli/api', 'tools', '/docs/sdk'],
+ ['/docs/wallet/recipes', 'tools', '/docs/sdk'],
+ ['/docs/sdk/typescript/api-client', '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/src/pages/404.tsx b/src/pages/404.tsx
new file mode 100644
index 000000000..5bff3280b
--- /dev/null
+++ b/src/pages/404.tsx
@@ -0,0 +1,5 @@
+import DocsNotFound from '../components/DocsNotFound'
+
+export default function NotFoundPage() {
+ return
+}
diff --git a/src/pages/_mdx-wrapper.tsx b/src/pages/_mdx-wrapper.tsx
index 4f5607c69..3a14dc4e0 100644
--- a/src/pages/_mdx-wrapper.tsx
+++ b/src/pages/_mdx-wrapper.tsx
@@ -40,8 +40,9 @@ export default function MDXWrapper({ children }: { children: ReactNode }) {
const context = MdxPageContext.use()
const frontmatter = context.frontmatter as Record | undefined
const needsProviders = Boolean(frontmatter?.interactive || frontmatter?.mipd)
- // The toolbar inserts its portal after the introduction. Mount it inside the
+ // The Copy page control inserts its portal beside the H1. Mount it inside the
// article's hydration boundary so it cannot change server HTML before React reads it.
+ // Tabs and the sidebar show the location, so pages carry no breadcrumbs.
const content = (
<>
{children}
diff --git a/src/pages/_slots.tsx b/src/pages/_slots.tsx
index ca790548a..371782844 100644
--- a/src/pages/_slots.tsx
+++ b/src/pages/_slots.tsx
@@ -1,6 +1,7 @@
-// Documentation pages use their authored links and sidebar for navigation.
+import DocsFooter from '../components/DocsFooter'
+
export function Footer() {
- return null
+ return
}
export const OutlineFooter = undefined
diff --git a/src/pages/docs/_layout.tsx b/src/pages/docs/_layout.tsx
index 169d59527..f95a81fc3 100644
--- a/src/pages/docs/_layout.tsx
+++ b/src/pages/docs/_layout.tsx
@@ -3,8 +3,6 @@
import { lazy, type PropsWithChildren, Suspense } from 'react'
import DocsHeader from '../../components/DocsHeader'
import DocsPageActions from '../../components/DocsPageActions'
-import DocsSectionNav from '../../components/DocsSectionNav'
-import DocsSidebarDrawer from '../../components/DocsSidebarDrawer'
import { usePageSettled } from '../../lib/pageSettled'
import { normalizeRscFetchUrl } from '../../lib/rsc-route-normalization'
import * as ui from './_layout.recipes'
@@ -59,8 +57,6 @@ export default function DocsLayout(
return (
<>
-
-
{props.children}
diff --git a/src/pages/get-started.mdx b/src/pages/get-started.mdx
index dd3c99335..1b954913b 100644
--- a/src/pages/get-started.mdx
+++ b/src/pages/get-started.mdx
@@ -6,6 +6,7 @@ ogImage: /og/2026-q4/og_2026-q4_developers_get-started_title-f5_v02.png
---
import { Card, Cards } from 'vocs'
+import { HomeGuideButton } from '../components/DocsHomeLinks'
import { DocsSetupCards } from '../components/DocsSetupCards'
@@ -22,7 +23,7 @@ Tempo is a blockchain for stablecoin financial products. Give customers accounts
-Send your first test payment with the [interactive quickstart](/get-started/quickstart).
+
diff --git a/src/pages/index.mdx b/src/pages/index.mdx
index 01e3084c2..88516adba 100644
--- a/src/pages/index.mdx
+++ b/src/pages/index.mdx
@@ -1,5 +1,5 @@
---
-title: "Documentation"
+title: "Build stablecoin payments on Tempo"
seoTitle: "Tempo Developer Docs: APIs, SDKs & Guides"
description: Build with Tempo Accounts, Earn, Routes, and Zones. Try developer walkthroughs, build machine payments, and explore the network and API reference.
ogImage: /og/2026-q4/og_2026-q4_developers_developers_title-f5_v02.png
@@ -8,9 +8,7 @@ ogImage: /og/2026-q4/og_2026-q4_developers_developers_title-f5_v02.png
import { Badge } from '../components/Badge'
import { ChevronLink } from '../components/ChevronLink'
import { tempoDocsHome } from '../components/DocsHome.styles'
-
-import { DocsHomeAgent } from '../components/DocsHomeAgent'
-import { HomeColorLink, HomeGuideButton } from '../components/DocsHomeLinks'
+import { HomeGuideButton } from '../components/DocsHomeLinks'
import { DocsHomeProductIcon } from '../components/DocsHomeProductIcon'
@@ -20,17 +18,19 @@ import { DocsHomeProductIcon } from '../components/DocsHomeProductIcon'
-# Documentation
+
Documentation
-Build financial products with Tempo's APIs, SDKs, and protocol.
+# Build stablecoin payments on Tempo
-
+Guides, API reference, and SDKs for building financial products with Tempo.
-
+
+
+
-
+
-
+
@@ -49,7 +49,7 @@ Build financial products with Tempo's APIs, SDKs, and protocol.
Send stablecoins with a test wallet.
-
+Run the quickstart
@@ -58,21 +58,20 @@ Send stablecoins with a test wallet.
Choose passkeys, signing keys, or a wallet provider.
-
+Choose a signing method
-### Pay for an API
+### Build with AI
-Find a service and make a paid request with the CLI.
+Connect your coding agent with the Tempo MCP server and skills.
-
+Set up your agent
-
@@ -155,13 +154,13 @@ Create accounts, send and receive payments, and control who can use them.
-### Tempo EVM
+### Network
Primitives
-Build wallets, issue stablecoins, and deploy Solidity contracts on Tempo.
+Build on the Tempo EVM, run a node and follow network upgrades.
-
Deploy a contract
-
TIP-20 Tokens
@@ -195,59 +194,58 @@ Pay for APIs from code or the CLI, and accept payments from agents.
+
+
+
+
+
+
+
+
+
+
-## Tools & references
+## Reference & tools
-
-
+
+
-
+
-### SDKs & CLI
+Network details
-- All SDKs
-- TypeScript
-- Rust
-- Go
-- Python
-- Foundry
-- Tempo CLI
+RPC endpoints, chain IDs, and explorers.
-
+
-
-
-
+
-### Protocol & infrastructure
+Testnet faucet
-- Protocol reference
-- Stablecoins on Tempo
-- JSON-RPC reference
-- Run a node
-- Changelog
+Get free test stablecoins on testnet.
-
+
-
-
-
-
-
-
+
+
+API reference
+
+Endpoints, authentication, and conventions.
-### APIs & partners
+
+
+
+
-- Tempo API
-- API keys
-- Console guide
-- Partners
-- Open source on GitHub
+SDKs & CLI
+
+TypeScript, Rust, Go, Python, Foundry, and the Tempo CLI.
+
diff --git a/src/styles/contract.ts b/src/styles/contract.ts
index f2ce44ecd..be53dbc75 100644
--- a/src/styles/contract.ts
+++ b/src/styles/contract.ts
@@ -84,6 +84,9 @@ export const design = extendVars(palette, {
code: 'var(--font-jetbrains-mono)',
sansFallback: 'var(--font-pilat-book), sans-serif',
system: 'var(--font-pilat-book), ui-sans-serif, system-ui, sans-serif',
+ // DS Web copy (Inter, loaded by @tempoxyz/ds): docs prose and the chrome around it, which DS
+ // components share. Titles keep Pilat (`book`).
+ ui: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
},
letterSpacing: {
normal: '0',
diff --git a/src/styles/copyFeedback.ts b/src/styles/copyFeedback.ts
index 043e67466..da2a9c66d 100644
--- a/src/styles/copyFeedback.ts
+++ b/src/styles/copyFeedback.ts
@@ -9,7 +9,7 @@ const iconIn = keyframes({
})
global({
- '.tempo-agent-start-copy, .docs-header-agent-panel, .docs-header-mobile-agents': {
+ '.tempo-agent-start-copy': {
'--tempo-check-color': 'var(--vocs-text-color-primary)',
},
'.tempo-icon-swap': {
diff --git a/src/styles/docsGetStarted.ts b/src/styles/docsGetStarted.ts
index f2daec449..12e01c26d 100644
--- a/src/styles/docsGetStarted.ts
+++ b/src/styles/docsGetStarted.ts
@@ -1,27 +1,16 @@
import { global } from 'zyzz/web'
-import { vars as tokens } from './theme'
// GS1: the Get Started hero. The page opens with its own #get-started anchor,
-// which scopes these rules to it. The lede is narrower and in the primary color,
-// and the page actions sit closer to the lede and to the quickstart line below.
-// At 400px the lede ran four lines and left its last word alone; 560px sets it in
-// three, and pretty wrapping keeps the last line from being a single word.
+// which scopes this rule to it. The lead keeps the shared lead colour (secondary)
+// and rhythm (lead → first content 32); it is only narrower. At 400px it ran four
+// lines and left its last word alone; 560px sets it in three, and pretty wrapping
+// keeps the last line from being a single word.
global({
'@layer utilities': {
- 'article[data-v-content]:has(> [id="get-started"]:first-child) > h1[data-v] + p[data-v]': {
- maxWidth: '560px',
- textWrap: 'pretty',
- marginBottom: tokens.spacing['4'],
- color: tokens.color.foreground,
- },
+ 'article[data-v-content]:has(> [id="get-started"]:is(:first-child, .docs-breadcrumbs + *)) > h1[data-v] + p[data-v]':
+ {
+ maxWidth: '560px',
+ textWrap: 'pretty',
+ },
},
})
-
-// The page-actions recipe is unlayered, so this override is too. Without the
-// recipe's bottom padding, its own margin sets the gap to the quickstart line.
-global({
- 'article[data-v-content]:has(> [id="get-started"]:first-child) > .docs-page-actions-host > .docs-page-actions':
- {
- paddingBottom: tokens.spacing['0'],
- },
-})
diff --git a/src/styles/docsSearch.ts b/src/styles/docsSearch.ts
index 3deb150b9..c3ca8528a 100644
--- a/src/styles/docsSearch.ts
+++ b/src/styles/docsSearch.ts
@@ -19,7 +19,7 @@ global({
// width that globals.ts gives docs layouts (--vocs-spacing-content there, 985.7px at
// 16px Pilat). The dialog is portalled to , outside that layout, so the same
// expression is repeated here.
-// It takes the smoothed panel radius. G4: the light dialog matches the page and
+// It takes the dialog radius (16). G4: the light dialog matches the page and
// keeps a hairline; the dark one is lifted to neutral 088 (above the #141414
// panels it covers) and drops it.
global({
@@ -27,8 +27,8 @@ global({
'[data-base-ui-portal] > [role="dialog"]:has(input[role="combobox"])': {
maxWidth: 'calc(84ch + (var(--vocs-spacing-content-px) * 2))',
borderColor: tokens.color.hairline,
- borderRadius: tokens.radius.panel,
- '--corner-radius': tokens.radius.panel,
+ borderRadius: tokens.radius.xl,
+ '--corner-radius': tokens.radius.xl,
// design-exception: TDS shadow.primary (shade 008) under the lighter scrim; Vocs used shadow-2xl.
boxShadow: '0 16px 48px rgb(0 0 0 / 0.08) !custom',
},
diff --git a/src/styles/docsSteps.ts b/src/styles/docsSteps.ts
index 60ca30144..6d8fc0bc5 100644
--- a/src/styles/docsSteps.ts
+++ b/src/styles/docsSteps.ts
@@ -1,13 +1,19 @@
import { global } from 'zyzz/web'
import { vars as tokens } from './theme'
-// QS2: every step after the first starts 48px below the previous one (Vocs
-// spaces them 24px). Each step is a wrapper that opens with its heading, and
-// sibling margins collapse, so this sets the full gap above steps 2 onward.
+// Steps follow the block rhythm: 24 between steps and 8 from a step's title to its
+// body. Vocs spaces the steps 24 apart; its title sits 24 above a body that pulls up
+// 8, so both margins are set here. The connector (the steps' left rule) is unchanged.
global({
'@layer utilities': {
'[data-v-steps] > * + *': {
- marginTop: tokens.spacing['12'],
+ marginTop: tokens.spacing['6'],
+ },
+ '[data-v-steps] > * > [data-v-step-title]': {
+ marginBottom: tokens.spacing['2'],
+ },
+ '[data-v-steps] > * > [data-v-step-content]': {
+ marginTop: tokens.spacing['0'],
},
},
})
diff --git a/src/styles/globals.ts b/src/styles/globals.ts
index b8845986f..5fa62f7bf 100644
--- a/src/styles/globals.ts
+++ b/src/styles/globals.ts
@@ -112,15 +112,13 @@ global({
'@layer utilities': {
':root': {
'--tempo-docs-outline-width': '240px',
- '--tempo-docs-primary-nav-height': '65px',
- '--tempo-docs-section-nav-height': '44px',
+ '--tempo-docs-primary-nav-height': '56px',
'--tempo-docs-shell-width': '100%',
'--tempo-docs-sidebar-width': '256px',
'--tempo-hub-width': '1168px',
'--tempo-font-display': 'var(--font-pilat-book)',
'--tempo-font-body': 'var(--font-pilat-book)',
- '--vocs-spacing-topNav':
- 'calc( var(--tempo-docs-primary-nav-height) + var(--tempo-docs-section-nav-height) )',
+ '--vocs-spacing-topNav': 'var(--tempo-docs-primary-nav-height)',
},
},
})
@@ -252,53 +250,12 @@ global({
// G11 callouts: see the TDS Callout and Alert treatment at the end of this file.
+// The outline is the rail on wide screens only. Below it, the menu drawer holds the section's
+// pages and the page has no outline, so Vocs' mobile row ("Menu", "On this page") stays hidden.
global({
'@layer utilities': {
- '[data-docs-sidebar-toggle]': {
- display: 'inline-flex',
- },
- '[data-docs-sidebar-toggle]:not([data-docs-sidebar-fallback])': {
- marginInlineEnd: tokens.spacing['2'],
- },
- },
-})
-
-global({
- '@layer utilities': {
- '@media (width >= 1080px)': {
- '[data-docs-sidebar-toggle]': {
- display: 'none',
- },
- },
- },
-})
-
-global({
- '@layer utilities': {
- '[data-docs-sidebar-fallback]': {
- position: 'sticky',
- zIndex: tokens.zIndex.raised,
- top: 'calc(var(--vocs-spacing-topNav) + var(--vocs-spacing-banner))',
- display: 'flex',
- alignItems: 'center',
- height: '48px',
- // design-exception: Preserve the inherited component/framework scope at the point of use.
- paddingInline: 'var(--vocs-spacing-content-px) !custom',
- borderBottomWidth: tokens.borderWidth.hairline,
- borderBottomStyle: 'solid',
- borderBottomColor: inherited.color.vocsBorderColorPrimary,
- backgroundColor: inherited.color.vocsBackgroundColorPrimary,
- fontSize: tokens.fontSize.compact,
- },
- },
-})
-
-global({
- '@layer utilities': {
- '@media (width >= 1080px)': {
- '[data-docs-sidebar-fallback]': {
- display: 'none',
- },
+ '[data-v-outline-mobile]': {
+ display: 'none',
},
},
})
@@ -367,7 +324,7 @@ global({
global({
'@layer utilities': {
'@media (width >= 1376px)': {
- '[data-v-sidebar] [data-v-content], [data-v-sidebar] [data-v-footer]': {
+ '[data-v-sidebar] [data-v-content]': {
marginLeft: 'auto !custom',
marginRight: 'auto !custom',
},
@@ -424,7 +381,7 @@ global({
'@layer utilities': {
'[data-v-sidebar-container] [data-v-sidebar]': {
gap: tokens.spacing['6'],
- fontFamily: tokens.fontFamily.system,
+ fontFamily: tokens.fontFamily.ui,
},
},
})
@@ -439,61 +396,64 @@ global({
global({
'@layer utilities': {
+ // Groups sit 24 apart (the sidebar gap) with no dividers.
'[data-v-sidebar] > [data-v-sidebar-section] + [data-v-sidebar-section]': {
- borderTopWidth: tokens.borderWidth.hairline,
- borderTopStyle: 'solid',
- borderTopColor: inherited.color.colorMixInSrgbForeground12Transparent,
- paddingTop: tokens.spacing['6'],
+ borderTopWidth: tokens.borderWidth.none,
+ paddingTop: tokens.spacing['0'],
},
},
})
-// SB2: a single-section sidebar's header is its title, set like the right rail's
-// "On this page" label in the secondary color. In multi-section sidebars every
-// header is a group label (for example Accounts, Payments, Fees): entry type and
-// spacing in the secondary color. Collapsible headers are interactive, so they
-// keep the entry (primary) color. Entries use the primary color.
+// SB2: every section header is a group label (for example Accounts, Payments,
+// Fees), quieter than its items like DS SidebarMenu's GroupLabel: 12/16 in the
+// tertiary color, 4 above its first item. Labels start on the gutter with the items
+// (sidebar padding 12 + item padding 12). Collapsible headers are controls, so they
+// keep the 32px item height and the item style.
global({
'@layer utilities': {
'[data-v-sidebar] > [data-v-sidebar-section]:only-child > [data-v-sidebar-section-header]': {
height: 'auto',
minHeight: 0,
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- margin: '0 -8px 12px !custom',
+ // design-exception: Items bleed 12px into the sidebar padding so labels sit on the gutter.
+ margin: '0 -12px 4px !custom',
paddingBlock: tokens.spacing['0'],
- paddingInline: tokens.spacing['2'],
+ paddingInline: tokens.spacing['3'],
backgroundColor: 'transparent !custom',
- color: tokens.color.muted,
- fontSize: tokens.fontSize.compact,
- fontWeight: tokens.fontWeight.medium,
+ color: tokens.color.tertiary,
+ fontFamily: tokens.fontFamily.ui,
+ fontSize: tokens.fontSize.xs,
+ fontWeight: tokens.fontWeight.normal,
letterSpacing: tokens.letterSpacing.normal,
- lineHeight: tokens.lineHeight.normal,
+ lineHeight: tokens.lineHeight.caption,
textTransform: 'none',
},
'[data-v-sidebar] > [data-v-sidebar-section]:not(:only-child) > [data-v-sidebar-section-header], [data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-header]:not([data-collapsable="true"])':
{
height: 'auto',
- minHeight: '34px',
- // design-exception: Matches the sidebar entries' geometry below.
- margin: '0 -8px !custom',
+ minHeight: 0,
+ // design-exception: Matches the sidebar entries' 12px bleed below.
+ margin: '0 -12px 4px !custom',
- paddingBlock: tokens.spacing['1_5'],
- paddingInline: tokens.spacing['2'],
+ paddingBlock: tokens.spacing['0'],
+ paddingInline: tokens.spacing['3'],
backgroundColor: 'transparent !custom',
- color: tokens.color.muted,
- fontSize: tokens.fontSize.sm,
+ color: tokens.color.tertiary,
+ fontFamily: tokens.fontFamily.ui,
+ fontSize: tokens.fontSize.xs,
fontWeight: tokens.fontWeight.normal,
letterSpacing: tokens.letterSpacing.normal,
- lineHeight: tokens.lineHeight.normal,
+ lineHeight: tokens.lineHeight.caption,
textTransform: 'none',
},
'[data-v-sidebar] > [data-v-sidebar-section] > [data-v-sidebar-section-header][data-collapsable="true"]':
{
+ minHeight: '32px',
+ paddingBlock: tokens.spacing['1_5'],
color: tokens.color.foreground,
},
},
@@ -522,24 +482,24 @@ global({
global({
'@layer utilities': {
- '[data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-header]': {
- minHeight: '34px',
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- margin: '0 -8px !custom',
+ '[data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-header][data-collapsable="true"]':
+ {
+ minHeight: '32px',
+ // design-exception: Items bleed 12px into the sidebar padding so labels sit on the gutter.
+ margin: '0 -12px !custom',
- paddingBlock: tokens.spacing['1_5'],
- paddingInline: tokens.spacing['2'],
- // SB3: one radius step rounder than before, smoothed.
- borderRadius: tokens.radius.md,
- '--corner-radius': tokens.radius.md,
- backgroundColor: 'transparent !custom',
+ paddingBlock: tokens.spacing['1_5'],
+ paddingInline: tokens.spacing['3'],
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ backgroundColor: 'transparent !custom',
- color: tokens.color.foreground,
- fontSize: tokens.fontSize.sm,
- fontWeight: tokens.fontWeight.normal,
- letterSpacing: tokens.letterSpacing.normal,
- lineHeight: tokens.lineHeight.normal,
- },
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ letterSpacing: tokens.letterSpacing.normal,
+ lineHeight: tokens.lineHeight.control,
+ },
},
})
@@ -547,7 +507,7 @@ global({
'@layer utilities': {
'[data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-header][data-collapsable="true"]:hover':
{
- backgroundColor: inherited.color.colorMixInSrgbForeground4Transparent,
+ backgroundColor: tokens.color.containerSubtle,
color: tokens.color.foreground,
},
},
@@ -558,41 +518,47 @@ global({
'[data-v-sidebar] [data-v-sidebar-section-content]': {
display: 'flex',
flexDirection: 'column',
- gap: 0,
+ gap: tokens.spacing['0_5'],
},
},
})
global({
'@layer utilities': {
+ // Nested items indent 12, with no guide line.
'[data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-content]': {
marginTop: tokens.spacing['0_5'],
- marginBottom: tokens.spacing['0_5'],
-
- borderLeftColor: inherited.color.colorMixInSrgbForeground10Transparent,
- gap: 0,
+ marginBottom: tokens.spacing['0'],
+ paddingInlineStart: tokens.spacing['3'],
+ borderLeftWidth: tokens.borderWidth.none,
+ gap: tokens.spacing['0_5'],
},
},
})
global({
'@layer utilities': {
+ // Items: 32 high, radius 8, label in the secondary color on the gutter.
'[data-v-sidebar] [data-v-sidebar-item][data-link]': {
- minHeight: '34px',
- // design-exception: Preserve this component’s existing geometry; it is not a shared scale step.
- margin: '0 -8px !custom',
+ minHeight: '32px',
+ // design-exception: Items bleed 12px into the sidebar padding so labels sit on the gutter.
+ margin: '0 -12px !custom',
paddingBlock: tokens.spacing['1_5'],
- paddingInline: tokens.spacing['2'],
- // SB3: one radius step rounder than before, smoothed.
+ paddingInline: tokens.spacing['3'],
borderRadius: tokens.radius.md,
'--corner-radius': tokens.radius.md,
- color: tokens.color.foreground,
+ color: tokens.color.muted,
+ fontFamily: tokens.fontFamily.ui,
fontSize: tokens.fontSize.sm,
fontWeight: tokens.fontWeight.normal,
letterSpacing: tokens.letterSpacing.normal,
- lineHeight: tokens.lineHeight.normal,
+ lineHeight: tokens.lineHeight.control,
+ },
+ // Collapsible headers read as items, in the same UI font.
+ '[data-v-sidebar] [data-v-sidebar-section-header][data-collapsable="true"]': {
+ fontFamily: tokens.fontFamily.ui,
},
},
})
@@ -619,7 +585,7 @@ global({
},
'[data-v-sidebar] > [data-v-sidebar-section] + [data-v-sidebar-section] > [data-v-sidebar-section-header][data-collapsable="true"]:hover':
{
- backgroundColor: inherited.color.colorMixInSrgbForeground4Transparent,
+ backgroundColor: tokens.color.containerSubtle,
color: tokens.color.foreground,
},
'@media (prefers-reduced-motion: reduce)': {
@@ -633,8 +599,9 @@ global({
global({
'@layer utilities': {
+ // Hover: the container/subtle chip.
'[data-v-sidebar] a[data-v-sidebar-item][data-link]:hover': {
- backgroundColor: inherited.color.colorMixInSrgbForeground4Transparent,
+ backgroundColor: tokens.color.containerSubtle,
color: tokens.color.foreground,
},
},
@@ -642,8 +609,9 @@ global({
global({
'@layer utilities': {
+ // Current: the container/regular chip, primary and medium (one active state everywhere).
'[data-v-sidebar] a[data-v-sidebar-item][data-active]': {
- backgroundColor: inherited.color.colorMixInSrgbForeground5Transparent,
+ backgroundColor: tokens.color.container,
color: tokens.color.foreground,
fontWeight: tokens.fontWeight.medium,
boxShadow: 'none',
@@ -651,46 +619,56 @@ global({
},
})
-// SB1: the current page has no hover state; hovering it keeps its fill.
+// A collapsible group whose own page is current takes the same chip as a current item. The
+// selectors out-rank the nested-header rules above, which reset the header's fill.
global({
'@layer utilities': {
- '[data-v-sidebar] a[data-v-sidebar-item][data-active]:hover': {
- backgroundColor: inherited.color.colorMixInSrgbForeground5Transparent,
- },
+ '[data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-header][data-collapsable][data-active]:has(> a[aria-current="page"]), [data-v-sidebar] > [data-v-sidebar-section] > [data-v-sidebar-section-header][data-collapsable][data-active]:has(> a[aria-current="page"])':
+ {
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ backgroundColor: tokens.color.container,
+ color: tokens.color.foreground,
+ fontWeight: tokens.fontWeight.medium,
+ },
+ '[data-v-sidebar] [data-v-sidebar-section-content] [data-v-sidebar-section-header][data-collapsable][data-active]:has(> a[aria-current="page"]):hover, [data-v-sidebar] > [data-v-sidebar-section] > [data-v-sidebar-section-header][data-collapsable][data-active]:has(> a[aria-current="page"]):hover':
+ {
+ backgroundColor: tokens.color.container,
+ },
},
})
+// SB1: the current page has no hover state; hovering it keeps its fill.
global({
'@layer utilities': {
- '[data-v-sidebar] [data-v-sidebar-item][data-condensed="true"]': {
- minHeight: '28px',
- fontSize: tokens.fontSize.compact,
+ '[data-v-sidebar] a[data-v-sidebar-item][data-active]:hover': {
+ backgroundColor: tokens.color.container,
},
},
})
global({
'@layer utilities': {
- '[data-v-sidebar] [data-v-sidebar-section-header] svg': {
- color: inherited.color.colorMixInSrgbForeground38Transparent,
+ '[data-v-sidebar] [data-v-sidebar-item][data-condensed="true"]': {
+ minHeight: '32px',
+ fontSize: tokens.fontSize.sm,
},
},
})
global({
'@layer utilities': {
- '[data-v-sidebar-footer-content] [data-v-socials] a[aria-label="GitHub"] svg': {
- width: '19px',
- height: '19px',
+ '[data-v-sidebar] [data-v-sidebar-section-header] svg': {
+ color: tokens.color.muted,
},
},
})
global({
'@layer utilities': {
- '[data-v-sidebar-footer-content] [data-v-socials] a[aria-label="X (Twitter)"] svg': {
- width: '16px',
- height: '16px',
+ // GitHub and X live in the site footer; the sidebar footer keeps only the theme control.
+ '[data-v-sidebar-footer-content] [data-v-socials]': {
+ display: 'none',
},
},
})
@@ -717,41 +695,11 @@ global({
},
})
+// Ask AI lives in each page's header row (DocsPageActions), which also takes its ⌘I shortcut.
global({
'@layer utilities': {
- '@media (width < 1376px)': {
- '[data-v-ask-ai-container]': {
- display: 'none',
- },
- },
- },
-})
-
-global({
- '@layer utilities': {
- '@media (width >= 1376px)': {
- '[data-v-ask-ai-container]': {
- position: 'fixed',
- top: 'auto',
- bottom: '24px',
- left: 'auto',
- right: 'max(24px, calc((100% - var(--tempo-docs-shell-width)) * 0.5 + 24px))',
- width: 'calc(var(--tempo-docs-outline-width) - 48px)',
- translate: 0,
- transform: 'none',
- },
- },
- },
-})
-
-global({
- '@layer vocs_utilities': {
- '@media (width >= 1376px)': {
- '[data-v-ask-ai-container] > button': {
- minWidth: 0,
- width: '100%',
- maxWidth: '100%',
- },
+ '[data-v-ask-ai-container]': {
+ display: 'none',
},
},
})
@@ -1023,7 +971,7 @@ global({
})
global({
- 'html:has(.docs-section-nav), body:has(.docs-section-nav)': {
+ 'html:has(.docs-site-header), body:has(.docs-site-header)': {
backgroundColor: tokens.color.shell,
},
})
@@ -1042,6 +990,18 @@ global({
},
})
+// Docs pages set copy in Inter, like DS Body: Vocs' prose, lists, tables and callouts, and
+// everything that inherits the body font. Titles keep Pilat (--tempo-font-display), code the
+// mono. Scoped to the docs shell, so the marketing pages keep Pilat.
+global({
+ ':root:has(.docs-site-header)': {
+ '--vocs-font-family': tokens.fontFamily.ui,
+ },
+ 'body:has(.docs-site-header)': {
+ fontFamily: tokens.fontFamily.ui,
+ },
+})
+
global({
':where( a[href], button:not(:disabled), summary, [role="button"], input[type="button"], input[type="submit"], input[type="reset"] )':
{
@@ -1232,38 +1192,74 @@ global({
global({
'@layer utilities': {
- 'article[data-v-content] :is(h1, h2, h3)[data-v]': {
+ // Headings: Pilat Book, regular, tight tracking, primary. H2 24, H3 18, H4 16.
+ 'article[data-v-content] :is(h1, h2, h3, h4)[data-v]': {
+ color: tokens.color.foreground,
fontFamily: inherited.fontFamily.tempoFontDisplay,
- fontWeight: tokens.fontWeight.medium,
+ fontWeight: tokens.fontWeight.normal,
- letterSpacing: tokens.letterSpacing.heading,
+ letterSpacing: tokens.letterSpacing.tight,
textWrap: 'balance',
},
+ 'article[data-v-content] h2[data-v]': {
+ fontSize: tokens.fontSize.title,
+ lineHeight: tokens.lineHeight.heading,
+ },
+ 'article[data-v-content] h3[data-v]': {
+ fontSize: tokens.fontSize.lead,
+ lineHeight: tokens.lineHeight.snug,
+ },
+ 'article[data-v-content] h4[data-v]': {
+ fontSize: tokens.fontSize.body,
+ lineHeight: tokens.lineHeight.normal,
+ },
},
})
global({
'@layer utilities': {
- 'article[data-v-content] > h1[data-v]': {
- maxWidth: '24ch',
+ // H1 32 (28 below 768), with the Copy page control floated on its first line.
+ 'article[data-v-content] h1[data-v]': {
+ maxWidth: 'none',
border: 0,
paddingBottom: 0,
- // design-exception: Preserve this responsive geometry across viewport sizes.
- fontSize: 'clamp(32px, 3vw, 42px) !custom',
- letterSpacing: tokens.letterSpacing.display,
+ fontSize: tokens.fontSize.displaySmall,
+ letterSpacing: tokens.letterSpacing.tight,
- lineHeight: tokens.lineHeight.display,
+ lineHeight: tokens.lineHeight.heading,
+ },
+ '@media (width < 768px)': {
+ 'article[data-v-content] h1[data-v]': {
+ fontSize: tokens.fontSize.heading,
+ },
+ },
+ // H1 to lead 12; H2 16 below; H3 12 below; H4 8 below.
+ 'article[data-v-content] > h1[data-v]:has(+ p[data-v])': {
+ marginBottom: tokens.spacing['3'],
+ },
+ 'article[data-v-content] > h2[data-v]': {
+ marginBottom: tokens.spacing['4'],
+ },
+ 'article[data-v-content] > h3[data-v]': {
+ marginBottom: tokens.spacing['3'],
+ },
+ 'article[data-v-content] > h4[data-v]': {
+ marginBottom: tokens.spacing['2'],
+ },
+ // Lists: 8 between items.
+ 'article[data-v-content] :is(ul, ol)[data-v] > li + li': {
+ marginTop: tokens.spacing['2'],
},
},
})
global({
'@layer utilities': {
+ // Lead at body size in the secondary color, 32 above the first content.
'article[data-v-content] > h1[data-v] + p[data-v]': {
- color: inherited.color.colorMixInSrgbForeground68Transparent,
-
+ marginBottom: tokens.spacing['8'],
+ color: tokens.color.muted,
fontSize: tokens.fontSize.body,
- lineHeight: tokens.lineHeight.prose,
},
},
})
@@ -1288,16 +1284,44 @@ global({
},
})
+// The rail holds the outline only: "On this page" 14/20 secondary, items 14/20
+// secondary, the current one primary with a 2px indicator in content/primary on a
+// hairline track. Feedback moves to the page end; Copy page sits on the H1's row.
global({
'@layer utilities': {
+ '[data-v-outline]:not([data-v-outline-mobile]) > :not([data-v-outline-nav])': {
+ display: 'none',
+ },
+ '[data-v-outline-nav]': {
+ gap: tokens.spacing['3'],
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ },
+ '[data-v-outline-nav] > div:first-child': {
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.control,
+ },
+ '[data-v-outline-nav] > div:first-child > svg': {
+ display: 'none',
+ },
+ '[data-v-outline-items]': {
+ borderInlineStartColor: tokens.color.hairline,
+ fontSize: tokens.fontSize.sm,
+ },
+ '[data-v-outline-item] a': {
+ paddingBlock: tokens.spacing['1'],
+ color: tokens.color.muted,
+ fontWeight: tokens.fontWeight.normal,
+ lineHeight: tokens.lineHeight.control,
+ },
+ '[data-v-outline-item] a:hover': {
+ color: tokens.color.foreground,
+ },
'[data-v-outline-indicator]': {
backgroundColor: tokens.color.foreground,
},
- },
-})
-
-global({
- '@layer utilities': {
'[data-v-outline-item] a[data-active="true"]': {
color: tokens.color.foreground,
},
@@ -1307,7 +1331,7 @@ global({
global({
'@layer utilities': {
// The docs-local SegmentedControl and Tabs keep their own TDS rings (inset for segments).
- 'body:has(.docs-section-nav) :is(a, button, input, summary):not([data-tds-control]):focus-visible':
+ 'body:has(.docs-site-header) :is(a, button, input, summary):not([data-tds-control]):focus-visible':
{
outlineWidth: tokens.borderWidth.emphasis,
outlineStyle: 'solid',
@@ -1333,7 +1357,7 @@ global({
global({
'@layer utilities': {
- '[data-layout]:has(.tempo-docs-home) [data-v-gutter-left], [data-layout]:has(.tempo-docs-home) [data-v-gutter-right], [data-layout]:has(.tempo-docs-home) [data-v-outline-mobile], [data-layout]:has(.tempo-docs-home) [data-v-outline], [data-layout]:has(.tempo-docs-home) [data-v-content-footer], [data-layout]:has(.tempo-docs-home) [data-v-footer]':
+ '[data-layout]:has(.tempo-docs-home) [data-v-gutter-left], [data-layout]:has(.tempo-docs-home) [data-v-gutter-right], [data-layout]:has(.tempo-docs-home) [data-v-outline-mobile], [data-layout]:has(.tempo-docs-home) [data-v-outline], [data-layout]:has(.tempo-docs-home) [data-v-content-footer]':
{
display: 'none',
},
@@ -1342,11 +1366,22 @@ global({
global({
'@layer utilities': {
+ // Home sits on the gutter (16, 24 from 768) with 32 above the hero (48 from 1080).
'article[data-v-content]:has(.tempo-docs-home)': {
- maxWidth: 'calc(var(--tempo-hub-width) + 96px)',
+ maxWidth: 'calc(var(--tempo-hub-width) + 48px)',
marginInline: 'auto !custom',
- // design-exception: Preserve this responsive geometry across viewport sizes.
- padding: 'clamp(24px, 4vw, 48px) !custom',
+ paddingBlock: tokens.spacing['8'],
+ paddingInline: tokens.spacing['4'],
+ },
+ '@media (width >= 768px)': {
+ 'article[data-v-content]:has(.tempo-docs-home)': {
+ paddingInline: tokens.spacing['6'],
+ },
+ },
+ '@media (width >= 1080px)': {
+ 'article[data-v-content]:has(.tempo-docs-home)': {
+ paddingBlock: tokens.spacing['12'],
+ },
},
},
})
@@ -1435,10 +1470,50 @@ global({
borderWidth: tokens.borderWidth.none,
backgroundColor: tokens.color.block,
},
- // Vocs pulls the active tab's underline over the header border it no longer has;
- // keep it inside the tab list, which clips vertically.
+ // Code header tabs sit 4 inside the 12px block, so they take radius 8 (12 - 4): 32
+ // high, 14/20, secondary; the current tab is the container/regular chip.
+ '[data-v-code-group-list]': {
+ gap: tokens.spacing['1'],
+ paddingInline: tokens.spacing['1'],
+ },
'[data-v-code-group-tab]': {
+ height: '32px',
marginBottom: tokens.spacing['0'],
+ paddingInline: tokens.spacing['3'],
+ borderBottomWidth: tokens.borderWidth.none,
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ letterSpacing: tokens.letterSpacing.normal,
+ lineHeight: tokens.lineHeight.control,
+ },
+ '[data-v-code-group-tab]:hover': {
+ backgroundColor: tokens.color.containerSubtle,
+ color: tokens.color.foreground,
+ },
+ '[data-v-code-group-tab][data-active]': {
+ backgroundColor: tokens.color.container,
+ color: tokens.color.foreground,
+ fontWeight: tokens.fontWeight.medium,
+ },
+ '[data-v-code-title]': {
+ fontSize: tokens.fontSize.sm,
+ fontWeight: tokens.fontWeight.normal,
+ letterSpacing: tokens.letterSpacing.normal,
+ lineHeight: tokens.lineHeight.control,
+ },
+ // Table headers 12/16; kbd 4.
+ 'div[data-v-table-wrapper] th': {
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
+ },
+ 'article[data-v-content] kbd': {
+ borderRadius: tokens.radius.sm,
+ '--corner-radius': tokens.radius.sm,
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
},
// The API reference overview pane sits on its own fill.
'[data-v-openapi-overview]': {
@@ -1460,9 +1535,21 @@ global({
'@layer base': {
':is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > :not(:is(hgroup[data-v], h1[data-v])) + h2:not(:only-child)':
{
- // design-exception: Vocs sets this margin !important; the section token replaces it.
- marginTop: 'var(--tempo-section-space) !custom !important',
+ // design-exception: Vocs sets this margin !important; system-v2 puts 48 above an H2.
+ marginTop: '48px !custom !important',
},
+ 'article[data-v-content] > :not(:is(h1, h2))[data-v] + h3[data-v]': {
+ // design-exception: Vocs pulls prose siblings up 8px with !important; system-v2 puts 32 above an H3.
+ marginTop: '32px !custom !important',
+ },
+ 'article[data-v-content] > :not(:is(h1, h2, h3))[data-v] + h4[data-v]': {
+ // design-exception: Vocs pulls prose siblings up 8px with !important; system-v2 puts 24 above an H4.
+ marginTop: '24px !custom !important',
+ },
+ 'article[data-v-content] > :is(h3, h4)[data-v] + :is(p, ul, ol)[data-v]': {
+ // design-exception: Vocs pulls prose siblings up 8px with !important; the heading's own margin sets the gap.
+ marginTop: '0px !custom !important',
+ },
},
'@layer vocs_utilities': {
':is(article[data-v-content], [data-v-openapi-intro], [data-v-openapi-guide]) > :not(:is(hgroup[data-v], h1[data-v])) + h2:not(:only-child)':
@@ -1483,27 +1570,124 @@ global({
paddingTop: tokens.spacing['0'],
borderTopWidth: tokens.borderWidth.none,
},
- // The page footer (feedback, edit link, pager) starts a section's space below the body.
+ // Page end: 64 above one row with "Was this helpful?" on the left and "Edit this
+ // page" on the right; 16 to the divider; 24 to the Previous / Next pager. Vocs
+ // renders feedback here only below 1376px (the rail has it above) and adds a
+ // Last updated line and a second Copy for AI, which the page header replaces.
'article[data-v-content] > [data-v-content-footer]': {
- marginTop: tokens.spacing.section,
+ display: 'grid',
+ gridTemplateColumns: 'minmax(0, 1fr) auto',
+ alignItems: 'center',
+ columnGap: tokens.spacing['4'],
+ rowGap: tokens.spacing['4'],
+ marginTop: tokens.spacing['16'],
+ },
+ '[data-v-content-footer] > *': {
+ gridColumn: '1 / -1',
+ marginBlock: tokens.spacing['0'],
+ },
+ '[data-v-content-footer] > :first-child': {
+ display: 'block',
+ gridColumn: 1,
+ gridRow: 1,
+ },
+ '[data-v-content-footer] > :nth-child(2)': {
+ gridColumn: 2,
+ gridRow: 1,
+ flexDirection: 'row',
+ },
+ '[data-v-content-footer] > :first-child:empty, [data-v-content-footer] > :nth-child(2):empty': {
+ display: 'none',
+ },
+ '[data-layout]:has(.docs-page-actions) [data-v-content-footer] > :nth-child(3), [data-v-last-updated]':
+ {
+ display: 'none',
+ },
+ '[data-v-content-footer] > :last-child': {
+ paddingTop: tokens.spacing['6'],
+ borderTopColor: tokens.color.hairline,
+ },
+ '[data-v-content-footer] div[data-v-feedback]': {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: tokens.spacing['2'],
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ },
+ '[data-v-content-footer] div[data-v-feedback] > p': {
+ color: tokens.color.muted,
+ fontWeight: tokens.fontWeight.normal,
+ },
+ '[data-v-content-footer] div[data-v-feedback] > div': {
+ gap: tokens.spacing['2'],
+ },
+ '[data-v-content-footer] div[data-v-feedback] button': {
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ color: tokens.color.muted,
+ },
+ '[data-v-content-footer] form[data-v-feedback]': {
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ },
+ // Edit this page: a tertiary button with a pencil.
+ '[data-v-edit-link]': {
+ gap: tokens.spacing['2'],
+ height: '32px',
+ paddingInline: tokens.spacing['2'],
+ borderRadius: tokens.radius.md,
+ '--corner-radius': tokens.radius.md,
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
+ textDecoration: 'none',
+ },
+ '@media (hover: hover)': {
+ ':is([data-v-content-footer] div[data-v-feedback] button, [data-v-edit-link]):hover': {
+ backgroundColor: tokens.color.containerSubtle,
+ color: tokens.color.foreground,
+ },
+ '[data-v-pagination] > a:hover': {
+ backgroundColor: tokens.color.containerSubtle,
+ transitionDuration: 'var(--tempo-enter)',
+ },
+ },
+ // Previous and Next are surface cards: radius 12, padding 16 20, title 16, label 12.
+ '[data-v-pagination]': {
+ gap: tokens.spacing['4'],
},
- // Pager links dim on hover instead of turning accent.
'[data-v-pagination] > a': {
- transitionProperty: 'opacity',
+ flexGrow: 1,
+ flexBasis: 0,
+ gap: tokens.spacing['1'],
+ paddingBlock: tokens.spacing['4'],
+ paddingInline: tokens.spacing['5'],
+ borderWidth: tokens.borderWidth.hairline,
+ borderStyle: 'solid',
+ borderColor: tokens.color.hairline,
+ borderRadius: tokens.radius.lg,
+ '--corner-radius': tokens.radius.lg,
+ backgroundColor: tokens.color.background,
+ transitionProperty: 'color, background-color, border-color, opacity, transform',
transitionDuration: 'var(--tempo-exit)',
transitionTimingFunction: 'var(--tempo-ease)',
},
'[data-v-pagination] > a > span': {
transitionProperty: 'none',
},
- '[data-v-pagination] > a:hover > span:first-child': {
- color: inherited.color.vocsTextColorHeading,
- },
- '@media (hover: hover)': {
- '[data-v-pagination] > a:hover': {
- opacity: 0.56,
- transitionDuration: 'var(--tempo-enter)',
+ // Vocs puts the page title first and the Previous / Next label second.
+ '[data-v-pagination] > a > span:first-child, [data-v-pagination] > a:hover > span:first-child':
+ {
+ color: tokens.color.foreground,
+ fontSize: tokens.fontSize.body,
+ fontWeight: tokens.fontWeight.medium,
+ lineHeight: tokens.lineHeight.body,
},
+ '[data-v-pagination] > a > span:last-child': {
+ color: tokens.color.muted,
+ fontSize: tokens.fontSize.xs,
+ lineHeight: tokens.lineHeight.caption,
},
},
})
@@ -1523,18 +1707,19 @@ global({
},
})
-// G11: callouts follow TDS Platform. Notes and info use Callout (subtle fill, no
-// icon); tip, warning, danger and success use Alert (tone fill, an 8px tone
-// square in place of the icon). Neither has a border, and text stays primary.
+// G11: callouts follow the TDS Platform Alert in four quiet tones: neutral (note,
+// info), tip, warning and negative (danger), plus positive for success. Each has a
+// subtle tinted fill, no border and a leading icon in its tone; text stays primary.
global({
'@layer utilities': {
'aside[data-v][data-v-callout]': {
+ '--tempo-alert-tone': 'var(--foreground-secondary)',
+ position: 'relative',
marginInline: tokens.spacing['0'],
- paddingBlock: tokens.spacing['6'],
- paddingInline: tokens.spacing['8'],
+ padding: tokens.spacing['4'],
borderWidth: tokens.borderWidth.none,
- '--corner-radius': tokens.radius.xl,
- borderRadius: tokens.radius.xl,
+ '--corner-radius': tokens.radius.lg,
+ borderRadius: tokens.radius.lg,
backgroundColor: tokens.color.containerSubtle,
color: inherited.color.vocsTextColorPrimary,
fontSize: tokens.fontSize.sm,
@@ -1544,23 +1729,10 @@ global({
{
color: inherited.color.vocsTextColorPrimary,
},
- // TDS B2 lines (14/20) inside callouts, so the indicator centers on the first line.
+ // TDS B2 lines (14/20) inside callouts, so the icon centers on the first line.
'aside[data-v][data-v-callout] :is(p, li)[data-v]': {
lineHeight: tokens.lineHeight.control,
},
- 'aside[data-v][data-v-callout] > [data-v-callout-icon]': {
- display: 'none',
- },
- 'aside[data-v][data-v-callout][data-v-content] > :not(:is([data-v-callout-icon], [data-v-code-container]))':
- {
- paddingInlineStart: tokens.spacing['0'],
- },
- 'aside[data-v][data-v-callout]:is([data-v-context="tip"], [data-v-context="warning"], [data-v-context="danger"], [data-v-context="success"])':
- {
- padding: tokens.spacing['4'],
- '--corner-radius': tokens.radius.lg,
- borderRadius: tokens.radius.lg,
- },
'aside[data-v][data-v-callout][data-v-context="tip"]': {
'--tempo-alert-tone': 'var(--info)',
backgroundColor: tokens.color.infoContainer,
@@ -1577,36 +1749,27 @@ global({
'--tempo-alert-tone': 'var(--positive)',
backgroundColor: tokens.color.positiveContainer,
},
- 'aside[data-v][data-v-callout]:is([data-v-context="tip"], [data-v-context="warning"], [data-v-context="danger"], [data-v-context="success"]) > [data-v-callout-icon]':
- {
- display: 'block',
- width: tokens.spacing['2'],
- height: tokens.spacing['2'],
- // design-exception: Centres the 8px square on the 20px first line (20 − 8) / 2.
- marginTop: '6px !custom',
- marginInlineStart: tokens.spacing['1'],
- // The TDS indicator's 2px corner, smoothed.
- borderRadius: tokens.radius.xs,
- '--corner-radius': tokens.radius.xs,
- // design-exception: The tone is set per context above from the status tokens.
- backgroundColor: 'var(--tempo-alert-tone) !custom',
- },
+ 'aside[data-v][data-v-callout] > [data-v-callout-icon]': {
+ position: 'absolute',
+ // Centres the 16px icon on the 20px first line inside the 16px inset.
+ insetBlockStart: '18px',
+ insetInlineStart: '16px',
+ width: '16px',
+ height: '16px',
+ marginTop: tokens.spacing['0'],
+ // design-exception: The tone is set per context above from the status tokens.
+ color: 'var(--tempo-alert-tone) !custom',
+ },
'aside[data-v][data-v-callout] > [data-v-callout-icon] > svg': {
- display: 'none',
+ display: 'block',
+ width: '16px',
+ height: '16px',
},
- 'aside[data-v][data-v-callout]:is([data-v-context="tip"], [data-v-context="warning"], [data-v-context="danger"], [data-v-context="success"])[data-v-content] > :not(:is([data-v-callout-icon], [data-v-code-container]))':
+ 'aside[data-v][data-v-callout][data-v-content] > :not(:is([data-v-callout-icon], [data-v-code-container]))':
{
paddingInlineStart: tokens.spacing['7'],
},
},
- '@media (width < 768px)': {
- '@layer utilities': {
- 'aside[data-v][data-v-callout]': {
- paddingBlock: tokens.spacing['5'],
- paddingInline: tokens.spacing['5'],
- },
- },
- },
})
// G8: Vocs-owned buttons follow the same rule: gray secondary fill, no outline,
@@ -1638,14 +1801,67 @@ global({
})
// Body text takes TDS Platform B1's size and tracking (16px, 0.12px) in docs
-// articles. B1's 22px line height is set for UI text; long-form prose keeps the
-// Vocs 26px leading. Card descriptions keep their own 14px size, and callouts
-// keep B2 (utilities layer above). Home keeps its own scale.
+// articles. B1's 22px line height is set for UI text; long-form prose keeps a
+// 26px leading. Card descriptions keep their own 14px size, and callouts keep
+// B2 (utilities layer above). Home keeps its own scale.
global({
'@layer vocs_utilities': {
'article[data-v-content] :is(p, li)[data-v]:not(a *)': {
fontSize: tokens.fontSize.body,
+ lineHeight: tokens.lineHeight.relaxed,
letterSpacing: tokens.letterSpacing.b1,
},
},
})
+
+// Prose keeps a readable measure of about 72 characters; code blocks, tables,
+// callouts and cards still use the full content column.
+global({
+ '@layer utilities': {
+ ':is(article[data-v-content]:not(:has(.tempo-docs-home)), [data-v-openapi-intro], [data-v-openapi-guide]) > :is(p, ul, ol, dl, blockquote, hgroup)[data-v]':
+ {
+ maxWidth: '720px',
+ },
+ },
+})
+
+// The site footer is a band under the article. It starts at the sidebar's edge,
+// runs under the right rail to the page edge, and keeps its columns in line with
+// the article. Above 1376px it rises over the fixed outline rail as it scrolls in.
+global({
+ // Vocs caps API landing and guide footers at the content width with !important.
+ '@layer base': {
+ '[data-v-main] > [data-v-footer]': {
+ maxWidth: 'none !important',
+ },
+ },
+ '@layer utilities': {
+ '[data-v-main]:has(> [data-v-footer])': {
+ paddingBottom: tokens.spacing['0'],
+ },
+ '[data-v-main] > [data-v-footer]': {
+ width: 'auto',
+ maxWidth: 'none',
+ // The article's bottom padding sets the gap; the footer brings its own hairline.
+ marginTop: tokens.spacing['0'],
+ paddingInline: tokens.spacing['0'],
+ paddingBottom: tokens.spacing['0'],
+ },
+ '@media (width >= 1376px)': {
+ '[data-v-main] > [data-v-footer]': {
+ position: 'relative',
+ zIndex: tokens.zIndex.overlay,
+ },
+ '[data-layout][data-v-sidebar]:not(:has(.tempo-docs-home)):not([data-v-content-width="full"]:has([data-v-openapi]:not([data-v-openapi-landing]))) > [data-v-main] > [data-v-footer]':
+ {
+ // design-exception: Extend the band under the outline rail that the main column pads for.
+ marginInlineEnd: 'calc(var(--tempo-docs-outline-width) * -1) !custom',
+ },
+ '[data-layout][data-v-sidebar]:not(:has(.tempo-docs-home)):not([data-v-content-width="full"]:has([data-v-openapi]:not([data-v-openapi-landing]))) > [data-v-main] > [data-v-footer] > .docs-site-footer':
+ {
+ // design-exception: Keep the footer columns clear of the outline rail.
+ paddingInlineEnd: 'var(--tempo-docs-outline-width) !custom',
+ },
+ },
+ },
+})
diff --git a/src/styles/surfaces.styles.ts b/src/styles/surfaces.styles.ts
index 18a208551..2efcebd20 100644
--- a/src/styles/surfaces.styles.ts
+++ b/src/styles/surfaces.styles.ts
@@ -137,186 +137,6 @@ export const featureDiagramMark = style({
filter: 'var(--feature-diagram-mark-filter, url("#feature-diagram-glow"))',
})
-export const heading64 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.hero,
- fontWeight: tokens.fontWeight.semibold,
- lineHeight: tokens.lineHeight.display,
- letterSpacing: tokens.letterSpacing.normal,
- },
-})
-
-export const heading48 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.displayLarge,
- fontWeight: tokens.fontWeight.semibold,
-
- lineHeight: tokens.lineHeight.display,
- letterSpacing: tokens.letterSpacing.normal,
- },
-})
-
-export const heading40 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.display,
- fontWeight: tokens.fontWeight.semibold,
- lineHeight: tokens.lineHeight.heading,
- letterSpacing: tokens.letterSpacing.normal,
- },
-})
-
-export const heading32 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.displaySmall,
- fontWeight: tokens.fontWeight.medium,
-
- lineHeight: tokens.lineHeight.heading,
- letterSpacing: tokens.letterSpacing.normal,
- },
-})
-
-export const heading24 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.title,
- fontWeight: tokens.fontWeight.medium,
-
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.normal,
- },
-})
-
-export const heading20 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.subheading,
- fontWeight: tokens.fontWeight.medium,
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const heading16 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.body,
- fontWeight: tokens.fontWeight.medium,
- lineHeight: tokens.lineHeight.normal,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const copy16 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.body,
- fontWeight: tokens.fontWeight.normal,
- lineHeight: tokens.lineHeight.relaxed,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const copy15 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.bodySmall,
- fontWeight: tokens.fontWeight.normal,
- lineHeight: tokens.lineHeight.relaxed,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const copy14 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.sm,
- fontWeight: tokens.fontWeight.normal,
- lineHeight: tokens.lineHeight.relaxed,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const copy13 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.compact,
- fontWeight: tokens.fontWeight.normal,
-
- lineHeight: tokens.lineHeight.normal,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const label16 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.body,
- fontWeight: tokens.fontWeight.normal,
- lineHeight: tokens.lineHeight.normal,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const label15 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.bodySmall,
- fontWeight: tokens.fontWeight.normal,
-
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const label14 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.sm,
- fontWeight: tokens.fontWeight.normal,
-
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const label13 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.compact,
- fontWeight: tokens.fontWeight.normal,
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const label12 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.xs,
- fontWeight: tokens.fontWeight.normal,
-
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const button16 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.body,
- fontWeight: tokens.fontWeight.medium,
- lineHeight: tokens.lineHeight.normal,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const button14 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.sm,
- fontWeight: tokens.fontWeight.medium,
-
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
-export const button12 = style({
- '@layer utilities': {
- fontSize: tokens.fontSize.xs,
- fontWeight: tokens.fontWeight.medium,
-
- lineHeight: tokens.lineHeight.snug,
- letterSpacing: tokens.letterSpacing.wide,
- },
-})
-
export const indicatorFlow = style({
'@layer utilities': {
backgroundImage:
@@ -430,9 +250,8 @@ export const docsZoneDiagram = style({
'& figcaption': {
marginTop: tokens.spacing['3'],
color: tokens.color.muted,
-
- fontSize: tokens.fontSize.compact,
- lineHeight: tokens.lineHeight.normal,
+ fontSize: tokens.fontSize.sm,
+ lineHeight: tokens.lineHeight.control,
textAlign: 'center',
},
},
diff --git a/vocs.config.ts b/vocs.config.ts
index 67f12a5d1..d91974b63 100644
--- a/vocs.config.ts
+++ b/vocs.config.ts
@@ -9,6 +9,7 @@ import {
docsRouteDestination,
proxiedLegacyDocsRoutes,
} from './src/lib/docs-routing'
+import { docsSearch } from './src/lib/docs-search'
import { docsSections, docsUtilitySections } from './src/lib/docs-sections'
import { docsStructuredDataHead } from './src/lib/docs-structured-data'
import { createFeedbackAdapter } from './src/lib/feedback-adapter'
@@ -24,8 +25,6 @@ import { loadTempoOpenApi } from './src/lib/tempo-openapi'
const baseUrl = resolveBaseUrl()
const openApiSpecUrl = process.env.OPENAPI_SPEC_URL ?? 'https://api.tempo.xyz/openapi.json'
-const searchIndexFields = ['title', 'titles', 'subtitle', 'path', 'excerpt']
-const searchBoost = { title: 5, subtitle: 3, titles: 2, path: 3, excerpt: 3 }
// Page renders, Markdown exports, and builds share one GitHub request per cache window.
const tempoChangelog = cacheChangelog(
Changelog.github({ prereleases: true, repo: 'tempoxyz/tempo' }),
@@ -70,36 +69,6 @@ const changelog = Changelog.from({
},
})
-function extractSearchField(document: Record
, fieldName: string) {
- if (fieldName === 'path') {
- return String(document.href ?? '')
- .split('#')[0]
- .replace(/^\/docs\//, '/')
- .replaceAll('/', ' ')
- }
- if (fieldName === 'excerpt') {
- return String(document.text ?? '')
- .trim()
- .split(/\s+/)
- .slice(0, 24)
- .join(' ')
- }
- return document[fieldName]
-}
-
-function boostSearchDocument(
- _documentId: unknown,
- _term: string,
- storedFields?: Record,
-) {
- const priority = (storedFields?.searchPriority as number | undefined) ?? 1
- const href = storedFields?.href as string | undefined
- const segments = href ? href.split('/').filter(Boolean).length : 1
- const depth = href?.startsWith('/docs/') ? Math.max(segments - 1, 1) : segments
- const docsBoost = href?.startsWith('/docs/') ? 1.5 : 1
- return priority * (1 / Math.max(depth, 1)) * docsBoost
-}
-
// Proposed API reference grouping (IA comp). Product sections are named
// " API", chain reads sit in Data API, and the Console section follows
// the console.tempo.xyz sidebar.
@@ -207,7 +176,7 @@ export default defineConfig({
checkDeadlinks: true,
editLink: {
link: 'https://github.com/tempoxyz/docs/edit/main/src/pages/:path',
- text: 'Suggest changes to this page',
+ text: 'Edit this page',
},
title: 'Tempo Docs',
titleTemplate: (path, { frontmatter, title }) => {
@@ -249,20 +218,7 @@ export default defineConfig({
})
: undefined,
},
- search: {
- index: {
- fields: searchIndexFields,
- storeFields: ['path', 'excerpt'],
- extractField: extractSearchField,
- },
- query: {
- combineWith: 'OR',
- fuzzy: 0.1,
- prefix: true,
- boost: searchBoost,
- boostDocument: boostSearchDocument,
- },
- },
+ search: docsSearch,
sitemap: {
include: (path) =>
(path === '/' || /^\/(?:get-started|docs)(?:\/|$)/.test(path)) &&
@@ -306,7 +262,7 @@ export default defineConfig({
build: 'BUILD',
cli: 'CLI',
'developer-tools': 'DEVELOPER TOOLS',
- development: 'TEMPO EVM',
+ development: 'NETWORK',
earn: 'EARN',
ecosystem: 'ECOSYSTEM',
'get-started': 'GET STARTED',
@@ -351,10 +307,22 @@ export default defineConfig({
zones: 'ZONES',
}
+ // Network owns the node and upgrade pages that live under the guide and protocol paths.
+ const sectionPrefixMap: Record = {
+ '/guide/node': 'NETWORK',
+ '/protocol/upgrades': 'NETWORK',
+ }
+
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]
@@ -385,7 +353,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' }],
@@ -452,32 +420,17 @@ export default defineConfig({
{
text: 'Get Started',
items: [
- { text: 'Start building', link: '/get-started' },
+ { text: 'Build on Tempo', link: '/get-started' },
{ text: 'Quickstart', link: '/get-started/quickstart' },
{ text: 'Testnet faucet', link: '/docs/quickstart/faucet' },
+ { text: 'Network details', link: '/docs/quickstart/connection-details' },
{ text: 'Build with AI', link: '/docs/guide/using-tempo-with-ai' },
{ text: 'Stablecoins on Tempo', link: '/get-started/stablecoins' },
{ 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: [
@@ -508,127 +461,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',
},
],
},
@@ -768,6 +736,8 @@ export default defineConfig({
},
],
},
+ // The product switcher's Wallet link now lives with the accounts it signs for.
+ { text: 'Tempo Wallet', link: 'https://wallet.tempo.xyz' },
],
},
]
@@ -790,6 +760,7 @@ export default defineConfig({
{ text: 'One-time payments', link: '/docs/guide/machine-payments/one-time-payments' },
{ text: 'Payment sessions', link: '/docs/guide/machine-payments/pay-as-you-go' },
{ text: 'Streamed payments', link: '/docs/guide/machine-payments/streamed-payments' },
+ { text: 'MPP spec', link: 'https://mpp.dev' },
],
},
]
@@ -901,7 +872,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' },
@@ -919,7 +899,6 @@ export default defineConfig({
]
const nodeSidebar = {
text: 'Run a node',
- collapsed: true,
items: [
{
text: 'Overview',
@@ -943,6 +922,7 @@ export default defineConfig({
},
{
text: 'Run a validator',
+ collapsed: true,
items: [
{
text: 'Overview',
@@ -996,130 +976,74 @@ 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: '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' },
+ ],
+ }
// Keep guides and specifications in one chapter per topic. Group controls
// disclose pages; Vocs opens the active chapter without replacing the tree.
@@ -1280,7 +1204,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' },
@@ -1293,43 +1217,82 @@ 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 },
+ { text: 'Explorer', link: 'https://explore.tempo.xyz' },
],
},
]
- const toolsSidebar = developerToolsSidebar
+ // Network merges Tempo EVM and Operate: build on the chain, then run a node, then the changelog.
+ const networkSidebar = [...developersSidebar, nodeSidebar, changelogSidebar]
const partnerResourcesSidebar = [...ecosystemSidebar]
+ // Each product's sidebar ends with its API reference: the pages of its OpenAPI tags (see
+ // `apiTagGroups`). The API reference itself keeps the full sidebar generated from the spec.
+ const apiReference = (items: { text: string; link: string }[]) => ({
+ text: 'API reference',
+ items,
+ })
const productSidebars = {
- overview: getStartedSidebar,
- accounts: accountsSidebar,
- earn: earnSidebar,
- routes: routesSidebar,
- zones: zonesSidebar,
- developers: developersSidebar,
- 'machine-payments': machinePaymentsSidebar,
+ overview: [
+ ...getStartedSidebar,
+ apiReference([
+ { text: 'Overview', link: '/docs/api' },
+ { text: 'Authentication', link: '/docs/api/authentication' },
+ { text: 'API keys', link: '/docs/api/api-keys' },
+ { text: 'Faucet', link: '/docs/api/faucet' },
+ ]),
+ ],
+ accounts: [
+ ...accountsSidebar,
+ apiReference([
+ { text: 'Balances', link: '/docs/api/balances' },
+ { text: 'Activities', link: '/docs/api/activities' },
+ { text: 'Transactions', link: '/docs/api/transactions' },
+ { text: 'Transfers', link: '/docs/api/transfers' },
+ { text: 'Fee payer', link: '/docs/api/fee-payer-usage' },
+ ]),
+ ],
+ earn: [...earnSidebar, apiReference([{ text: 'Earn API', link: '/docs/api/earn' }])],
+ routes: [
+ ...routesSidebar,
+ apiReference([
+ { text: 'Supported routes', link: '/docs/api/routes/routes' },
+ { text: 'Deposits', link: '/docs/api/routes/deposit-addresses' },
+ { text: 'Transfers', link: '/docs/api/routes/transfers' },
+ { text: 'Exchange', link: '/docs/api/exchange' },
+ ]),
+ ],
+ zones: [...zonesSidebar, apiReference([{ text: 'Zones API', link: '/docs/api/zones' }])],
+ developers: [
+ ...networkSidebar,
+ apiReference([
+ { text: 'JSON-RPC', link: '/docs/api/rpc' },
+ { text: 'Blocks', link: '/docs/api/blocks' },
+ { text: 'Tokens', link: '/docs/api/tokens' },
+ { text: 'Verified tokens', link: '/docs/api/verified-tokens' },
+ { text: 'Indexer', link: '/docs/api/indexer' },
+ { text: 'Webhooks', link: '/docs/api/webhooks' },
+ { text: 'Fee AMM', link: '/docs/api/fee-amm' },
+ { text: 'CoinGecko', link: '/docs/api/coingecko' },
+ ]),
+ ],
+ 'machine-payments': [
+ ...machinePaymentsSidebar,
+ apiReference([{ text: 'MPP', link: '/docs/api/mpp' }]),
+ ],
api: [{ text: 'API reference', items: [{ text: 'Introduction', link: '/docs/api' }] }],
tools: toolsSidebar,
+ resources: resourcesSidebar,
ecosystem: partnerResourcesSidebar,
}
return {
- '/docs': getStartedSidebar,
+ '/docs': productSidebars.overview,
...Object.fromEntries(
[...docsSections, ...docsUtilitySections].flatMap((section) =>
section.matches.map((path) => [path, productSidebars[section.id]]),
),
),
- '/docs/guide/private-zones': zonesSidebar,
+ '/docs/guide/private-zones': productSidebars.zones,
'/docs/partners': partnerResourcesSidebar,
'/docs/partners/join': partnerResourcesSidebar,
'/docs/guide/ousd': partnerResourcesSidebar,