diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8c579f..2dc563b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,10 @@ jobs: - run: yarn build + # The option validator is tested against lib/, the compiled output + # consumers get, so this has to follow the build. + - run: yarn test + # `files` in package.json decides what reaches consumers, and it is the # one thing a successful `tsc` says nothing about. Pack the tarball and # assert the assets the design system references are inside it, so a file @@ -52,6 +56,7 @@ jobs: for asset in \ lib/index.cjs \ lib/index.d.cts \ + lib/options.cjs \ src/css/custom.css \ src/utils/rehypeTabsTransform.js \ static/fonts/Satoshi-Regular.woff \ diff --git a/MIGRATION.md b/MIGRATION.md index b60c8d1..ff24812 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,6 @@ # Migrating to @vantagecompute/docusaurus-theme -Two migrations live here: +Four migrations live here: - **[Part 1: adopting the theme](#part-1-adopting-the-theme)** walks the `vantage-docs` project through its first move onto the shared package. Any @@ -10,6 +10,9 @@ Two migrations live here: logo and its own navbar/footer logo config. Added in 0.4.7. - **[Part 3: small screens](#part-3-small-screens-049)** is for a site that papered over the theme's phone and tablet defects itself. Added in 0.4.9. +- **[Part 4: the theme-owned navbar](#part-4-the-theme-owned-navbar-050)** + is for a site on 0.4.x that declares its own `themeConfig.navbar` or + `footer`. Added in 0.5.0. ## Part 1: adopting the theme @@ -317,3 +320,73 @@ At 375, 768 and 1024 wide: - [ ] A wide reference table scrolls inside its frame rather than squeezing - [ ] Every tab in a tab strip is visible - [ ] At 1024 the article has no right-hand TOC column and shows the "On this page" collapsible instead + +## Part 4: the theme-owned navbar (0.5.0) + +From 0.5.0 the theme renders the navbar itself and renders no footer. A site's +`themeConfig.navbar` and `themeConfig.footer` are ignored, and the +`navbarLogo`, `footerLogo` and `ThemeLogo` exports are gone. This is the +breaking change behind the minor bump. + +### Step 1: Upgrade the package + +```bash +npm install @vantagecompute/docusaurus-theme@^0.5.0 +``` + +### Step 2: Move your external buttons to the theme option + +```diff +- themes: ['@docusaurus/theme-mermaid', '@vantagecompute/docusaurus-theme'], ++ themes: [ ++ '@docusaurus/theme-mermaid', ++ ['@vantagecompute/docusaurus-theme', { ++ navbarLinks: [ ++ {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, ++ {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, ++ ], ++ }], ++ ], +``` + +Two at most, `label` and `url` only. Anything else in your old `items` (doc +links, dropdowns, a search item) has no equivalent; the developer navbar does +not carry them, by design. + +### Step 3: Delete the navbar and footer blocks + +```diff +- const {staticDir, navbarLogo, footerLogo, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); ++ const {staticDir, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); + + themeConfig: { +- navbar: { +- title: 'my-project', +- logo: navbarLogo, +- items: [...], +- }, +- footer: {...}, + prism: {...}, + }, +``` + +The centred title now comes from `siteConfig.title`, so make sure that is the +name you want beside the version badge. The version badge still reads +`customFields.projectVersion`. + +### Step 4: Delete any local navbar swizzle + +If your `src/theme/` has `Navbar/Content`, `Navbar/Logo`, +`Navbar/MobileSidebar/PrimaryMenu` or `Footer`, delete them; a local copy +silently wins over the theme's. + +### Step 5: Verify + +```bash +npm run build +``` + +Then open the site: the brand mark links to `/developer/` (or the docs root +on the main site), your buttons open in a new tab with the external-link icon, +and there is no footer. A misconfigured `navbarLinks` fails the build with a +message naming the entry. diff --git a/README.md b/README.md index d510834..a6ae56f 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ Add the theme to your `docusaurus.config.js`: const { staticDir, rehypeTabsTransform, - navbarLogo, - footerLogo, getProjectVersion, } = require('@vantagecompute/docusaurus-theme'); @@ -30,8 +28,16 @@ const config = { // grows and shrinks reflows the header on every release. tagline: `What this project does (${projectVersion})`, - // Add the Vantage theme - themes: ['@vantagecompute/docusaurus-theme'], + // Add the Vantage theme. Its one option is the external buttons on the + // developer navbar: at most two, each a label and an absolute url. + themes: [ + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, + {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, + ], + }], + ], // Serve shared static assets (fonts, icons, brand mark) staticDirectories: ['static', staticDir], @@ -47,24 +53,13 @@ const config = { ], themeConfig: { - navbar: { - title: 'my-project', - logo: navbarLogo, - items: [/* ... */], - }, - footer: { - style: 'dark', - logo: footerLogo, - links: [/* ... */], - }, + // No navbar and no footer here: the theme renders both. A site under + // /developer/ gets the developer navbar, anything else the public one. + prism: {/* ... */}, }, }; ``` -`navbarLogo` and `footerLogo` carry the Vantage brand mark, its alt text, and -the right link target for each position. Your site needs no copy of the SVG: -it is served out of `staticDir`. - ## What's included ### Design System CSS @@ -79,21 +74,26 @@ Served from the package once `staticDir` is in your `staticDirectories`, so no s - **Fonts**: Satoshi (Regular, Medium, Bold + italics) as `.woff` files - **Icons**: Sun/moon toggles, search, external link, GitHub, chevron SVGs - **Brand mark**: `vantage-logo-color.svg`, the current Vantage mark, used by - `navbarLogo` and `footerLogo`. It has no dark variant on purpose: the one - colour mark is drawn to read in both colour modes. + the theme's navbar. It has no dark variant on purpose: the one colour mark + is drawn to read in both colour modes. - **Legacy logo**: `vantage-logo.svg`, the older monochrome mark. Kept for the `vantage-docs` hub, which still points at it. New sites should use the brand mark above. - **Favicon**: `favicon.ico` ### Theme Component Overrides -| Component | Description | +| Component | What it changes | |---|---| -| `ColorModeToggle` | Custom sun/moon SVG icon toggle | -| `DocBreadcrumbs` | Full-path breadcrumb rendering | -| `Navbar/Logo` | Centered site title with the version badge beside it | -| `Tabs` | Bugfix for Docusaurus 3.10 whitespace crash | -| `Navbar/MobileSidebar/SecondaryMenu` | Clean secondary menu render | +| `Navbar/Content` | The whole navbar, in a public or a developer variant chosen from `baseUrl` (0.5.0) | +| `Navbar/Logo` | The brand link with the variant's baked href, the centred title and the version badge | +| `Navbar/MobileSidebar/PrimaryMenu` | The developer navbar's external buttons, in the mobile drawer (0.5.0) | +| `Navbar/SiteActions` | An empty slot in the public navbar for a site's own controls (0.5.0) | +| `Navbar/MobileSidebar/SecondaryMenu` | A clean secondary-menu render | +| `Footer` | Renders nothing (0.5.0) | +| `ColorModeToggle` | Sun and moon SVG icons in place of the default toggle | +| `DocBreadcrumbs` | Full-path breadcrumbs instead of the truncated default | +| `Tabs` | A workaround for a Docusaurus 3.10 crash | +| `MDXComponents` | Every markdown `table` renders inside a horizontal scroll region (0.4.9) | ### Utilities | Export | Description | @@ -101,8 +101,7 @@ Served from the package once `staticDir` is in your `staticDirectories`, so no s | `staticDir` | Absolute path to this package's `static/` directory; add it to `staticDirectories` | | `rehypeTabsTransform` | Rehype plugin that transforms lowercase ``/`` to React components | | `getProjectVersion()` | Project version inferred from git tags (`git describe --tags --always`), or `"dev"` | -| `navbarLogo` | Navbar logo config: the brand mark, linking to `https://docs.vantagecompute.ai` | -| `footerLogo` | Footer logo config: the same mark, linking to `https://vantagecompute.ai` | +| `resolveNavbarVariant(baseUrl)` | The rule that picks the public or developer navbar; exported for tooling | ## Customization @@ -117,19 +116,21 @@ your-docs-site/ index.js ``` -### Overriding the logo - -`navbarLogo` and `footerLogo` are plain objects. Spread one to change a field, -and leave the rest to the theme: +### Navbar buttons ```js -navbar: { - logo: { ...navbarLogo, href: 'https://docs.vantagecompute.ai/developer/' }, -}, +themes: [ + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, + {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, + ], + }], +], ``` -Spread rather than mutate: the objects are shared by everything that imports -them. To render no logo at all, just omit `logo`. +That is the whole navbar surface a site has. See the docs site's Customization +page for the public navbar's `SiteActions` slot. ### Extending CSS Add your own CSS in `src/css/custom.css` and reference it in your preset config. Your styles will layer on top of the shared design system: diff --git a/docs/superpowers/plans/2026-09-10-theme-owned-navbars.md b/docs/superpowers/plans/2026-09-10-theme-owned-navbars.md new file mode 100644 index 0000000..7456635 --- /dev/null +++ b/docs/superpowers/plans/2026-09-10-theme-owned-navbars.md @@ -0,0 +1,1485 @@ +# Theme-Owned Navbars Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** The theme renders the whole navbar itself, in one of two fixed variants (public and developer), and the only navbar knob a site keeps is a list of at most two external link buttons. No site renders a footer. + +**Architecture:** The theme's Node entry point validates a `navbarLinks` option, picks the navbar variant from the site's `baseUrl` (anything under `/developer/` is developer, everything else is public), and publishes `{variant, logoHref, navbarLinks}` to the client with `setGlobalData`. Four theme component overrides read that data with `usePluginData` and render the navbar, the mobile menu and the brand link, ignoring `themeConfig.navbar` entirely. A fifth override returns `null` for the footer. The public variant exposes one empty slot component, `Navbar/SiteActions`, that the main docs site swizzles to insert its Ask AI button. + +**Tech Stack:** Docusaurus 3.10 theme package (TypeScript entry point compiled by `tsc` to `lib/*.cjs`, plain JSX theme components under `src/theme/`), Node's built-in test runner for the validator, the package's own docs site under `docusaurus/` as the integration check. + +**Decision record:** https://claude.ai/code/artifact/5f4a93be-42aa-4c1f-9c53-d731ddbfb53e (decisions D1 to D5). Meeting: Docs Site Consistency & Embeddings, 2026-09-10. + +**Confirmed design choices (Bryan, 2026-09-10):** +1. Variant is inferred from `baseUrl`, no explicit option. +2. The theme owns navbar rendering through component overrides plus plugin options; sites stop declaring `themeConfig.navbar`. +3. The public navbar renders search itself and leaves a `Navbar/SiteActions` slot for the Ask AI button, which lives in vantage-docs. + +**Assumptions to flag in the PR description:** +- The developer variant shows `siteConfig.title` centred with the version badge. The public variant shows no title; it keeps the lone version badge the main site shows today when `customFields.projectVersion` is set. +- Logo hrefs are absolute (`https://docs.vantagecompute.ai/` and `https://docs.vantagecompute.ai/developer/`), matching the convention the removed `navbarLogo` export used. +- `navbarLogo`, `footerLogo` and `ThemeLogo` are removed, not deprecated. This ships as 0.5.0. +- Between merging this and releasing 0.5.0, the docs site under `docusaurus/` builds against 0.4.9 from npm with a config that declares no navbar. It renders a bare navbar for that window and nothing breaks. `just docs-pin 0.5.0` closes the window. + +--- + +## File structure + +**Create** +- `src/options.cts`: option types, `validateVantageThemeOptions`, `resolveNavbarVariant`, `LOGO_HREF`. Pure, no Docusaurus imports, so it is unit-testable. +- `test/options.test.cjs`: `node:test` cases against the compiled `lib/options.cjs`. +- `src/theme/Navbar/useVantageNavbar.js`: one client hook that turns plugin global data plus `siteConfig` into `{variant, logoHref, links, title, version}`, and `toNavbarItems(links)` which maps links to theme-classic `NavbarItem` props. +- `src/theme/Navbar/Content/index.jsx` and `styles.module.css`: the navbar, both variants. +- `src/theme/Navbar/MobileSidebar/PrimaryMenu/index.jsx`: the same external links in the mobile drawer. +- `src/theme/Navbar/SiteActions/index.jsx`: the empty slot the public variant renders. +- `src/theme/Footer/index.js`: returns `null`. + +**Modify** +- `src/index.cts`: accept `(context, options)`, publish global data, export `validateOptions`, drop the three logo exports. +- `src/theme/Navbar/Logo/index.jsx`: replace the wrapper with a full brand-link render that uses the baked href. +- `src/css/custom.css`: delete the footer tokens and the `.footer*` block. +- `package.json`: add the `test` script. +- `.github/workflows/ci.yml`: run the tests, and assert `lib/options.cjs` is in the tarball. +- `docusaurus/docusaurus.config.ts`: drop `themeConfig.navbar` and `footer`, pass `navbarLinks`. +- `README.md`, `MIGRATION.md`, `docusaurus/docs/usage.md`, `docusaurus/docs/migration.md`, `docusaurus/docs/reference/{exports,components,customization,assets}.md`: document the option, the new overrides, and the removals. + +**Not in this plan** (tracked separately, in vantage-docs): removing the `Ctrl+I` hint from `src/components/VantageAssistant/AssistantLauncher.tsx`, dropping the main site's navbar and logo config, and adding its `src/theme/Navbar/SiteActions/index.tsx` swizzle that renders `AskAIButton`. + +--- + +## Reference: how theme-classic does it today + +The override work replaces three theme-classic components. Their v3.10.2 sources, for the engineer's orientation: + +- `Navbar/Content` reads `useThemeConfig().navbar.items`, splits them left and right, and renders `NavbarMobileSidebarToggle`, `NavbarLogo`, the items, `NavbarColorModeToggle` (with a CSS-module class that hides it below 997px), and `NavbarSearch > SearchBar` when no item has `type: 'search'`. +- `Navbar/MobileSidebar/PrimaryMenu` renders the same items as `` inside `ul.menu__list`. +- `Logo` renders `` containing `div.navbar__logo > img` and `b.navbar__title`. + +The hamburger is hidden when `themeConfig.navbar.items` is empty AND the page has no docs sidebar (`useIsNavbarMobileSidebarDisabled` in theme-common). After this change every site's `items` is empty, so the hamburger, and with it the mobile external links, appear only on pages with a docs sidebar. That is every docs page, and it is accepted. + +`@docusaurus/Link` does not add `target="_blank"` for external hrefs on its own, so the theme sets `target` and `rel` on each external item explicitly. `NavbarNavLink` appends the external-link icon whenever `href` is external and `label` is set. + +--- + +### Task 1: Option validator and variant resolver (TDD) + +> **Executed note (2026-09-10):** the validator must always return an `id` +> (defaulting to `'default'`). When a plugin exports `validateOptions`, +> Docusaurus trusts the returned object to carry the instance id and names the +> plugin's generated data directory after it; without one the build dies in +> `path.join` with "Received undefined". The tests and the code below were +> amended accordingly after the first docs-site build exposed it. + +**Files:** +- Create: `src/options.cts` +- Create: `test/options.test.cjs` +- Modify: `package.json` (scripts) + +- [ ] **Step 1: Add the test script** + +In `package.json`, change the `scripts` block to: + +```json + "scripts": { + "build": "tsc", + "test": "node --test 'test/**/*.test.cjs'", + "prepublishOnly": "npm run build" + }, +``` + +- [ ] **Step 2: Write the failing tests** + +Create `test/options.test.cjs`: + +```js +// Runs against the compiled output, the same code consumers get. Build first: +// yarn build && yarn test +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + validateVantageThemeOptions, + resolveNavbarVariant, + LOGO_HREF, + MAX_NAVBAR_LINKS, +} = require('../lib/options.cjs'); + +const github = {label: 'GitHub', url: 'https://github.com/vantagecompute/vantage-mcp'}; +const pypi = {label: 'PyPI', url: 'https://pypi.org/project/vantage-mcp/'}; +const npm = {label: 'npm', url: 'https://www.npmjs.com/package/vantage-mcp'}; + +test('no options at all resolves to no links', () => { + assert.deepEqual(validateVantageThemeOptions(undefined), {navbarLinks: []}); + assert.deepEqual(validateVantageThemeOptions({}), {navbarLinks: []}); +}); + +test('one and two links pass through normalised', () => { + assert.deepEqual(validateVantageThemeOptions({navbarLinks: [github]}), { + navbarLinks: [github], + }); + assert.deepEqual(validateVantageThemeOptions({navbarLinks: [github, pypi]}), { + navbarLinks: [github, pypi], + }); +}); + +test('labels are trimmed', () => { + const out = validateVantageThemeOptions({navbarLinks: [{...github, label: ' GitHub '}]}); + assert.equal(out.navbarLinks[0].label, 'GitHub'); +}); + +test('a third link is rejected', () => { + assert.equal(MAX_NAVBAR_LINKS, 2); + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [github, pypi, npm]}), + /at most 2/, + ); +}); + +test('a link with an extra property is rejected', () => { + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{...github, icon: 'github'}]}), + /"icon"/, + ); +}); + +test('a link needs a non-empty label', () => { + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{url: github.url}]}), + /label/, + ); + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{label: ' ', url: github.url}]}), + /label/, + ); +}); + +test('a link needs an absolute http(s) url', () => { + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{label: 'Docs', url: '/docs'}]}), + /url/, + ); + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{label: 'Mail', url: 'mailto:x@y.z'}]}), + /url/, + ); +}); + +test('navbarLinks must be an array', () => { + assert.throws(() => validateVantageThemeOptions({navbarLinks: github}), /array/); +}); + +test('an unknown top-level option is rejected by name', () => { + assert.throws(() => validateVantageThemeOptions({logoUrl: '/'}), /"logoUrl"/); +}); + +test('options must be an object', () => { + assert.throws(() => validateVantageThemeOptions([github]), /object/); + assert.throws(() => validateVantageThemeOptions('nope'), /object/); +}); + +test('the Docusaurus plugin id passes through untouched', () => { + assert.deepEqual(validateVantageThemeOptions({id: 'default', navbarLinks: [github]}), { + id: 'default', + navbarLinks: [github], + }); +}); + +test('baseUrl under /developer/ is the developer variant', () => { + assert.equal(resolveNavbarVariant('/developer/'), 'developer'); + assert.equal(resolveNavbarVariant('/developer/vantage-mcp/'), 'developer'); + assert.equal(resolveNavbarVariant('/developer/docusaurus-theme/'), 'developer'); +}); + +test('every other baseUrl is the public variant', () => { + assert.equal(resolveNavbarVariant('/'), 'public'); + assert.equal(resolveNavbarVariant('/docs/'), 'public'); + assert.equal(resolveNavbarVariant('/developers/'), 'public'); +}); + +test('logo hrefs are baked per variant', () => { + assert.deepEqual(LOGO_HREF, { + public: 'https://docs.vantagecompute.ai/', + developer: 'https://docs.vantagecompute.ai/developer/', + }); +}); +``` + +- [ ] **Step 3: Run the tests to confirm they fail** + +```bash +yarn build && yarn test +``` + +Expected: every test errors with `Cannot find module '../lib/options.cjs'`. + +- [ ] **Step 4: Implement `src/options.cts`** + +```ts +/** + * Theme options and the navbar variant rule. Pure functions with no Docusaurus + * imports, so they are tested directly against the compiled output. + */ + +export type NavbarVariant = 'public' | 'developer'; + +/** One external button in the developer navbar. Nothing else is configurable. */ +export interface NavbarLink { + label: string; + url: string; +} + +/** What a site may pass in `themes: [['@vantagecompute/docusaurus-theme', {...}]]`. */ +export interface VantageThemeOptions { + /** External buttons, at most {@link MAX_NAVBAR_LINKS}. Rendered by the developer navbar only. */ + navbarLinks?: NavbarLink[]; +} + +/** Options after validation. `id` is Docusaurus's plugin-instance id, kept if it was given. */ +export interface ResolvedVantageThemeOptions { + id?: string; + navbarLinks: NavbarLink[]; +} + +/** Two is GitHub plus one registry (PyPI, npm, ...). More than that is a nav, and navs drift. */ +export const MAX_NAVBAR_LINKS = 2; + +/** + * Where the brand mark links, per variant. Baked in on purpose: a site cannot + * point its logo anywhere else, which is what keeps the hub consistent. + */ +export const LOGO_HREF: Record = { + public: 'https://docs.vantagecompute.ai/', + developer: 'https://docs.vantagecompute.ai/developer/', +}; + +/** + * Every site published under /developer/ (the developer overview and every + * spoke) gets the developer navbar. Everything else gets the public one. + * Docusaurus normalises baseUrl to a leading and trailing slash before the + * plugin sees it. + */ +export function resolveNavbarVariant(baseUrl: string): NavbarVariant { + return baseUrl.startsWith('/developer/') ? 'developer' : 'public'; +} + +const PREFIX = '[@vantagecompute/docusaurus-theme]'; + +function fail(message: string): never { + throw new Error(`${PREFIX} ${message}`); +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function validateLink(value: unknown, index: number): NavbarLink { + const where = `navbarLinks[${index}]`; + if (!isPlainObject(value)) { + fail(`${where} must be an object with "label" and "url".`); + } + + const extra = Object.keys(value).filter((key) => key !== 'label' && key !== 'url'); + if (extra.length > 0) { + fail( + `${where} has unsupported propert${extra.length === 1 ? 'y' : 'ies'} ` + + `${extra.map((k) => `"${k}"`).join(', ')}. Only "label" and "url" are ` + + `accepted; the icon, external-link marker and rel attributes come from the theme.`, + ); + } + + const {label, url} = value; + if (typeof label !== 'string' || label.trim() === '') { + fail(`${where}.label must be a non-empty string.`); + } + if (typeof url !== 'string' || url.trim() === '') { + fail(`${where}.url must be a non-empty string.`); + } + + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return fail(`${where}.url must be an absolute http(s) URL, got "${url}".`); + } + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + fail(`${where}.url must be an absolute http(s) URL, got "${url}".`); + } + + return {label: label.trim(), url}; +} + +/** + * Validate and normalise the theme options. Throws with a message that names + * the offending key, so a misconfigured spoke fails its build instead of + * quietly rendering something off-convention. + */ +export function validateVantageThemeOptions(options: unknown): ResolvedVantageThemeOptions { + if (options === undefined || options === null) { + return {navbarLinks: []}; + } + if (!isPlainObject(options)) { + fail('theme options must be an object.'); + } + + const {navbarLinks, id, ...unknown} = options; + const unknownKeys = Object.keys(unknown); + if (unknownKeys.length > 0) { + fail( + `unknown option${unknownKeys.length === 1 ? '' : 's'} ` + + `${unknownKeys.map((k) => `"${k}"`).join(', ')}. The only option is "navbarLinks".`, + ); + } + + const resolved: ResolvedVantageThemeOptions = {navbarLinks: []}; + if (typeof id === 'string') { + resolved.id = id; + } + + if (navbarLinks === undefined) { + return resolved; + } + if (!Array.isArray(navbarLinks)) { + fail('"navbarLinks" must be an array of {label, url} objects.'); + } + if (navbarLinks.length > MAX_NAVBAR_LINKS) { + fail( + `"navbarLinks" allows at most ${MAX_NAVBAR_LINKS} entries, got ${navbarLinks.length}. ` + + `GitHub plus one package registry is the intended use.`, + ); + } + + resolved.navbarLinks = navbarLinks.map(validateLink); + return resolved; +} +``` + +Note on `id`: Docusaurus can hand a plugin its instance id inside `options` before `validateOptions` runs. The validator lets that one key through so the theme never fails on Docusaurus's own bookkeeping. + +- [ ] **Step 5: Run the tests to confirm they pass** + +```bash +yarn build && yarn test +``` + +Expected: `# pass 14` and `# fail 0`. Also confirm `ls lib/` shows `options.cjs` and `options.d.cts`. + +- [ ] **Step 6: Commit** + +```bash +git add package.json src/options.cts test/options.test.cjs +git commit -m "feat: validate navbarLinks and resolve the navbar variant from baseUrl" +``` + +--- + +### Task 2: Entry point publishes global data and drops the logo exports + +**Files:** +- Modify: `src/index.cts` + +- [ ] **Step 1: Rewrite `src/index.cts`** + +Replace the whole file with: + +```ts +import path from 'node:path'; +import {execSync} from 'node:child_process'; +import type {LoadContext, OptionValidationContext, Plugin} from '@docusaurus/types'; +import { + LOGO_HREF, + resolveNavbarVariant, + validateVantageThemeOptions, + type ResolvedVantageThemeOptions, + type VantageThemeOptions, +} from './options.cjs'; + +export type {NavbarLink, NavbarVariant, VantageThemeOptions} from './options.cjs'; +export {LOGO_HREF, MAX_NAVBAR_LINKS, resolveNavbarVariant} from './options.cjs'; + +/** + * Shape of the global data the theme's client components read through + * `usePluginData('@vantagecompute/docusaurus-theme')`. + */ +export interface VantageThemeGlobalData { + variant: 'public' | 'developer'; + logoHref: string; + navbarLinks: ResolvedVantageThemeOptions['navbarLinks']; +} + +export default function themeVantage( + context: LoadContext, + options: ResolvedVantageThemeOptions, +): Plugin { + const variant = resolveNavbarVariant(context.baseUrl); + const globalData: VantageThemeGlobalData = { + variant, + logoHref: LOGO_HREF[variant], + navbarLinks: options.navbarLinks, + }; + + return { + name: '@vantagecompute/docusaurus-theme', + + getThemePath() { + return path.resolve(__dirname, '../src/theme'); + }, + + getPathsToWatch() { + return [path.resolve(__dirname, '../src/theme/**/*.{js,jsx,ts,tsx,css}')]; + }, + + getClientModules() { + return [path.resolve(__dirname, '../src/css/custom.css')]; + }, + + // The navbar variant and the external buttons reach the client this way. + // Nothing in themeConfig.navbar is read by the theme's components. + contentLoaded({actions}) { + actions.setGlobalData(globalData); + }, + }; +} + +/** + * Docusaurus calls this before the plugin factory. Hand-rolled rather than Joi + * so the package carries no validation dependency; see src/options.cts. + */ +export function validateOptions({ + options, +}: OptionValidationContext): + ResolvedVantageThemeOptions { + return validateVantageThemeOptions(options); +} + +/** + * Returns the absolute path to this package's static directory. + * Add this to your `staticDirectories` in docusaurus.config.js: + * + * ```js + * const { staticDir } = require('@vantagecompute/docusaurus-theme'); + * module.exports = { + * staticDirectories: ['static', staticDir], + * }; + * ``` + */ +export const staticDir = path.resolve(__dirname, '../static'); + +/** + * Infer the project version from git tags via `git describe --tags --always`. + * Returns a string like "v0.3.1" (on a tag) or "v0.3.1-3-gabcdef" (between tags). + * Falls back to "dev" if git is unavailable or no tags exist. + */ +export function getProjectVersion(): string { + try { + const version = execSync('git describe --tags --always', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + }).trim(); + return version; + } catch { + return 'dev'; + } +} + +// Re-export the rehype utility (plain JS, lives in src/utils/) +export const rehypeTabsTransform = require(path.resolve(__dirname, '../src/utils/rehypeTabsTransform')); +``` + +`navbarLogo`, `footerLogo`, `ThemeLogo` and `VANTAGE_LOGO_SRC` are gone. The brand mark path now lives in the `Navbar/Logo` component (Task 4). + +- [ ] **Step 2: Build and confirm the exports** + +```bash +yarn build && node -e "const t=require('./lib/index.cjs'); console.log(Object.keys(t).sort().join(' '))" +``` + +Expected output contains `LOGO_HREF MAX_NAVBAR_LINKS default getProjectVersion rehypeTabsTransform resolveNavbarVariant staticDir validateOptions` and does not contain `navbarLogo` or `footerLogo`. + +- [ ] **Step 3: Smoke-test the factory** + +```bash +node -e " +const t=require('./lib/index.cjs'); +const opts=t.validateOptions({options:{navbarLinks:[{label:'GitHub',url:'https://github.com/x/y'}]}}); +const p=t.default({baseUrl:'/developer/x/'},opts); +let data; p.contentLoaded({actions:{setGlobalData:(d)=>{data=d}}}); +console.log(JSON.stringify(data)); +" +``` + +Expected: `{"variant":"developer","logoHref":"https://docs.vantagecompute.ai/developer/","navbarLinks":[{"label":"GitHub","url":"https://github.com/x/y"}]}` + +- [ ] **Step 4: Run the tests again** + +```bash +yarn test +``` + +Expected: `# pass 14`. + +- [ ] **Step 5: Commit** + +```bash +git add src/index.cts +git commit -m "feat: publish navbar variant and links as theme global data; drop logo exports" +``` + +--- + +### Task 3: The client hook + +**Files:** +- Create: `src/theme/Navbar/useVantageNavbar.js` + +- [ ] **Step 1: Create the hook** + +```js +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {usePluginData} from '@docusaurus/useGlobalData'; + +/** + * Everything the theme's navbar components need, in one place. The variant, + * the brand-link href and the external buttons come from the theme's Node + * side (setGlobalData in lib/index.cjs). The title and version come from the + * site config. themeConfig.navbar is deliberately never read. + */ +export function useVantageNavbar() { + const {siteConfig} = useDocusaurusContext(); + const {variant, logoHref, navbarLinks} = usePluginData('@vantagecompute/docusaurus-theme'); + + const raw = siteConfig.customFields?.projectVersion; + const version = raw + ? String(raw).startsWith('v') + ? String(raw) + : `v${raw}` + : null; + + return { + variant, + logoHref, + links: navbarLinks, + // The developer navbar names the project beside its version, which is + // how a reader confirms they are on the version they think they are. + // The public navbar is logo-only. + title: variant === 'developer' ? siteConfig.title : null, + version, + }; +} + +/** + * Map the validated {label, url} links onto theme-classic NavbarItem props. + * External destinations open in a new tab, the accepted convention for + * leaving a site. NavbarNavLink appends the external-link icon on its own + * because href is external and label is set. + */ +export function toNavbarItems(links) { + return links.map((link) => ({ + label: link.label, + href: link.url, + position: 'right', + target: '_blank', + rel: 'noopener noreferrer', + className: 'navbar__external-link', + })); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/theme/Navbar/useVantageNavbar.js +git commit -m "feat: add the useVantageNavbar client hook" +``` + +--- + +### Task 4: Brand link with the baked href + +**Files:** +- Modify: `src/theme/Navbar/Logo/index.jsx` (replace entirely) + +- [ ] **Step 1: Replace the component** + +The current file wraps `@theme-init/Navbar/Logo` and reads `themeConfig.navbar.title`. Replace the whole file with: + +```jsx +import React from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import {useVantageNavbar} from '../useVantageNavbar'; + +// The one colour mark reads on both colour modes, so there is no srcDark and +// no second asset to keep in sync. Resolves through staticDir, which every +// Vantage site lists in staticDirectories. +const LOGO_SRC = 'img/vantage-logo-color.svg'; + +/** + * The brand link, the centred title and the version badge. + * + * Replaces theme-classic's Logo outright instead of wrapping it, because the + * href is not the site's to choose: the theme bakes it per variant (the docs + * root for the public navbar, the developer overview for the developer one). + * A plain anchor rather than @docusaurus/Link because the jump crosses SPA + * boundaries and should be a full navigation. Same tab: command-click covers + * "open in a new tab". + * + * The markup mirrors theme-classic's (navbar__brand > navbar__logo > img, + * plus b.navbar__title) so Infima's and this theme's CSS keep applying. On + * desktop the in-brand title is hidden by CSS and re-rendered centred below, + * which is what lets the version badge sit beside it. + */ +export default function NavbarLogo() { + const {logoHref, title, version} = useVantageNavbar(); + const src = useBaseUrl(LOGO_SRC); + + return ( + <> + +
+ Vantage Compute Logo +
+ {title && {title}} +
+ {(title || version) && ( +
+ {title && {title}} + {version && {version}} +
+ )} + + ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/theme/Navbar/Logo/index.jsx +git commit -m "feat: render the brand link with the variant's baked href" +``` + +--- + +### Task 5: Navbar content, both variants + +**Files:** +- Create: `src/theme/Navbar/Content/index.jsx` +- Create: `src/theme/Navbar/Content/styles.module.css` +- Create: `src/theme/Navbar/SiteActions/index.jsx` + +- [ ] **Step 1: Create the slot** + +`src/theme/Navbar/SiteActions/index.jsx`: + +```jsx +/** + * A slot in the public navbar, between search and the colour-mode toggle. + * Empty here. The main docs site overrides it in its own src/theme/ to render + * its Ask AI button; that component talks to the Vantage AI backend and does + * not belong in a theme package. + * + * The developer navbar does not render this slot. + */ +export default function NavbarSiteActions() { + return null; +} +``` + +- [ ] **Step 2: Create the stylesheet** + +`src/theme/Navbar/Content/styles.module.css`, the one rule theme-classic's version carries. Keep the local class name `colorModeToggle`: the design system targets it with `[class*="colorModeToggle"]`. + +```css +/* The mobile drawer header has its own toggle. */ +@media (max-width: 996px) { + .colorModeToggle { + display: none; + } +} +``` + +- [ ] **Step 3: Create the navbar** + +`src/theme/Navbar/Content/index.jsx`: + +```jsx +import React from 'react'; +import clsx from 'clsx'; +import {ErrorCauseBoundary, ThemeClassNames} from '@docusaurus/theme-common'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import NavbarItem from '@theme/NavbarItem'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import SearchBar from '@theme/SearchBar'; +import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; +import NavbarLogo from '@theme/Navbar/Logo'; +import NavbarSearch from '@theme/Navbar/Search'; +import NavbarSiteActions from '@theme/Navbar/SiteActions'; +import {toNavbarItems, useVantageNavbar} from '../useVantageNavbar'; + +import styles from './styles.module.css'; + +/** + * The navbar, replacing theme-classic's Navbar/Content. + * + * Two fixed variants, chosen by the theme from the site's baseUrl: + * + * public logo | search, SiteActions slot, colour-mode toggle + * developer logo + centred title and version | external buttons, toggle + * + * themeConfig.navbar.items is never read. A site that still declares items + * gets no error and no rendering; the convention is the theme's to hold. + */ +function ExternalLinks({items}) { + return ( + <> + {items.map((item, i) => ( + + new Error( + `A theme navbar link failed to render: ${JSON.stringify(item)}`, + {cause: error}, + ) + }> + + + ))} + + ); +} + +function NavbarContentLayout({left, right}) { + return ( +
+
+ {left} +
+
+ {right} +
+
+ ); +} + +export default function NavbarContent() { + const mobileSidebar = useNavbarMobileSidebar(); + const {variant, links} = useVantageNavbar(); + + const left = ( + <> + {!mobileSidebar.disabled && } + + + ); + + const right = + variant === 'developer' ? ( + <> + + + + ) : ( + <> + + + + + + + ); + + return ; +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/theme/Navbar/Content src/theme/Navbar/SiteActions +git commit -m "feat: theme-owned navbar with public and developer variants" +``` + +--- + +### Task 6: Mobile drawer carries the same external links + +**Files:** +- Create: `src/theme/Navbar/MobileSidebar/PrimaryMenu/index.jsx` + +- [ ] **Step 1: Create the component** + +```jsx +import React from 'react'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import NavbarItem from '@theme/NavbarItem'; +import {toNavbarItems, useVantageNavbar} from '../../useVantageNavbar'; + +/** + * The primary panel of the mobile drawer. theme-classic fills it from + * themeConfig.navbar.items; this theme fills it from the same external + * buttons the developer navbar shows. The public navbar has nothing to put + * here (search and the site's actions stay in the bar), so the panel is empty + * and the drawer opens straight onto the docs sidebar. + */ +export default function NavbarMobilePrimaryMenu() { + const mobileSidebar = useNavbarMobileSidebar(); + const {variant, links} = useVantageNavbar(); + + if (variant !== 'developer' || links.length === 0) { + return null; + } + + return ( +
    + {toNavbarItems(links).map((item, i) => ( + mobileSidebar.toggle()} key={i} /> + ))} +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/theme/Navbar/MobileSidebar/PrimaryMenu +git commit -m "feat: mirror the external navbar links in the mobile drawer" +``` + +--- + +### Task 7: No footer + +**Files:** +- Create: `src/theme/Footer/index.js` +- Modify: `src/css/custom.css` (delete the footer token comment lines, the `Footer defaults` token block, and the `.footer*` rule block) + +- [ ] **Step 1: Create the override** + +`src/theme/Footer/index.js`: + +```js +/** + * No Vantage documentation site renders a footer. The collapse-sidebar control + * already frames the bottom of the screen, and the links a footer would carry + * are the navbar's external buttons. A site that still declares + * themeConfig.footer builds fine and renders nothing. + */ +export default function Footer() { + return null; +} +``` + +- [ ] **Step 2: Delete the footer CSS** + +Three edits in `src/css/custom.css`: + +1. In the token documentation comment near the top of the navbar section, delete these six lines: + +``` + * --vantage-footer-bg Footer background colour + * --vantage-footer-text Footer text colour + * --vantage-footer-title-color Footer section-title colour + * --vantage-footer-link-hover Footer link hover colour + * --vantage-footer-separator Footer separator colour + * --vantage-footer-logo-max-h Footer logo max-height +``` + +2. In the `:root` block that follows, delete the `/* Footer defaults */` comment and the six `--vantage-footer-*` declarations, plus the blank line before the comment. + +3. Delete the block that begins with the line `/* ── Footer ────────────────────────────────────────────────────────── */` and ends with the closing brace of `[data-theme='light'] .footer__link-social img { ... }`, plus the blank line after it. The next line kept must be `/* ── DocSearch (Algolia) - full theme ──────────────────────────────── */`. + +Do not touch any `.DocSearch-Footer` or `--docsearch-footer-*` rule; those style the search modal's footer, not the page footer. + +- [ ] **Step 3: Verify the CSS** + +```bash +grep -n "vantage-footer\|^\.footer" src/css/custom.css; echo "exit=$?" +``` + +Expected: no matches, `exit=1`. + +```bash +grep -c "DocSearch-Footer" src/css/custom.css +``` + +Expected: a number greater than 0 (unchanged). + +- [ ] **Step 4: Commit** + +```bash +git add src/theme/Footer src/css/custom.css +git commit -m "feat: render no footer, and drop the footer styles" +``` + +--- + +### Task 8: The docs site adopts the new contract + +**Files:** +- Modify: `docusaurus/docusaurus.config.ts` + +- [ ] **Step 1: Change the imports** + +Replace + +```ts +import { + staticDir, + navbarLogo, + footerLogo, + getProjectVersion, +} from '@vantagecompute/docusaurus-theme'; +``` + +with + +```ts +import {staticDir, getProjectVersion} from '@vantagecompute/docusaurus-theme'; +``` + +- [ ] **Step 2: Pass the theme options** + +Replace + +```ts + themes: ['@docusaurus/theme-mermaid', '@vantagecompute/docusaurus-theme'], +``` + +with + +```ts + // This site is a developer spoke (baseUrl under /developer/), so the theme + // renders its developer navbar: brand mark, centred title and version, and + // these two buttons. There is no themeConfig.navbar and no footer; both are + // the theme's, not the site's. See docs/reference/exports.md. + themes: [ + '@docusaurus/theme-mermaid', + [ + '@vantagecompute/docusaurus-theme', + { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/vantage-docusaurus-theme'}, + {label: 'npm', url: 'https://www.npmjs.com/package/@vantagecompute/docusaurus-theme'}, + ], + }, + ], + ], +``` + +- [ ] **Step 3: Remove the navbar and footer** + +In `themeConfig`, delete the entire `navbar: {...}` object and the entire `footer: {...}` object. `themeConfig` keeps `codeBlock`, `prism` and `tableOfContents`. The Docs and API links that were in the navbar are reachable from the sidebar (`sidebars.ts` lists `reference/exports`). + +- [ ] **Step 4: Typecheck the site against the working tree** + +The site installs the theme from npm by design, so link the working tree only for this check: + +```bash +yarn build && npm pack +``` + +Expected: a file `vantagecompute-docusaurus-theme-0.4.9.tgz` in the repo root (it is gitignored). + +```bash +cd docusaurus && npm install --no-save ../vantagecompute-docusaurus-theme-0.4.9.tgz && npm run typecheck +``` + +Expected: `tsc` exits 0. + +- [ ] **Step 5: Build the site and assert the rendered navbar** + +```bash +cd docusaurus && npm run build +``` + +Expected: `[SUCCESS] Generated static files in "build".` with no broken-link errors. + +```bash +cd docusaurus/build && \ +echo "developer logo href: $(grep -c 'class="navbar__brand" href="https://docs.vantagecompute.ai/developer/"' index.html)" && \ +echo "github button: $(grep -c 'href="https://github.com/vantagecompute/vantage-docusaurus-theme"[^>]*target="_blank"' index.html)" && \ +echo "npm button: $(grep -c 'npmjs.com/package/@vantagecompute/docusaurus-theme"[^>]*target="_blank"' index.html)" && \ +echo "rel noopener: $(grep -c 'rel="noopener noreferrer"' index.html)" && \ +echo "centred title: $(grep -c 'navbar__center-title-text' index.html)" && \ +echo "version badge: $(grep -c 'navbar__version-badge' index.html)" && \ +echo "site footer: $(grep -c 'theme-layout-footer' index.html)" +``` + +Expected: every count is 1 or more except `site footer`, which must be `0`. +(A bare ` string` | The project version from git tags. | +| `rehypeTabsTransform` | Rehype plugin | Lowercase ``/`` support in MDX. | +| `resolveNavbarVariant(baseUrl)` | `(string) => 'public' \| 'developer'` | The rule the theme applies to pick a navbar. Exported for tests and tooling. | +| `LOGO_HREF` | `Record` | Where the brand mark links, per variant. | +| `MAX_NAVBAR_LINKS` | `number` | Two. | +| `VantageThemeOptions`, `NavbarLink`, `NavbarVariant` | types | The option shapes. | +``` + +Update the two import examples below the table to `const {staticDir, getProjectVersion} = require(...)` and `import {staticDir, getProjectVersion} from ...`. + +Replace the `## default (the theme)` section's config snippet and the sentence "It takes no options..." with: + +```markdown +## `default` (the theme) + +```js +themes: [ + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/vantage-mcp'}, + {label: 'PyPI', url: 'https://pypi.org/project/vantage-mcp/'}, + ], + }], +], +``` + +The factory returns a Docusaurus plugin that does four things: + +- `getThemePath()` puts `src/theme/` into the theme resolution stack, which is + what makes the [component overrides](./components.md) take effect. +- `getClientModules()` returns `src/css/custom.css`, which is how the design + system loads. There is nothing to add to `customCss`. +- `getPathsToWatch()` covers `src/theme/**/*.{js,jsx,ts,tsx,css}`, so a linked + working tree hot-reloads during development. +- `contentLoaded()` publishes the navbar variant, the brand-link href and the + validated `navbarLinks` as plugin global data, which the navbar components + read. Nothing in `themeConfig.navbar` is read by the theme. + +### Options + +There is one. + +| Option | Type | Default | Meaning | +|---|---|---|---| +| `navbarLinks` | `{label: string, url: string}[]` | `[]` | External buttons on the right of the developer navbar. At most two. | + +Each entry is exactly `label` and `url`. The url must be absolute `http(s)`. +The theme adds the external-link icon, `target="_blank"` and +`rel="noopener noreferrer"`; a third entry, an extra property, a relative url +or an unknown option fails the build with a message naming the problem. + +The public navbar ignores `navbarLinks`; it has no buttons to add. + +### Which navbar a site gets + +The theme decides from `baseUrl`, and there is no override: + +| `baseUrl` | Variant | Brand link | Right-hand side | +|---|---|---|---| +| starts with `/developer/` | developer | `https://docs.vantagecompute.ai/developer/` | `navbarLinks` buttons, colour-mode toggle | +| anything else | public | `https://docs.vantagecompute.ai/` | search, the `Navbar/SiteActions` slot, colour-mode toggle | + +The developer navbar centres `siteConfig.title` with the version badge beside +it. The public navbar shows only the badge, when `customFields.projectVersion` +is set. + +Sites declare no `themeConfig.navbar` and no `themeConfig.footer`. A site that +still does builds fine; the theme renders neither. +``` + +Delete the `## navbarLogo and footerLogo` section and its `### ThemeLogo` subsection entirely. + +- [ ] **Step 2: `components.md`** + +Replace the overrides table with: + +```markdown +| Component | What it changes | +|---|---| +| `Navbar/Content` | The whole navbar, in a public or a developer variant chosen from `baseUrl` (0.5.0) | +| `Navbar/Logo` | The brand link with the variant's baked href, the centred title and the version badge | +| `Navbar/MobileSidebar/PrimaryMenu` | The developer navbar's external buttons, in the mobile drawer (0.5.0) | +| `Navbar/SiteActions` | An empty slot in the public navbar for a site's own controls (0.5.0) | +| `Navbar/MobileSidebar/SecondaryMenu` | A clean secondary-menu render | +| `Footer` | Renders nothing (0.5.0) | +| `ColorModeToggle` | Sun and moon SVG icons in place of the default toggle | +| `DocBreadcrumbs` | Full-path breadcrumbs instead of the truncated default | +| `Tabs` | A workaround for a Docusaurus 3.10 crash | +| `MDXComponents` | Every markdown `table` renders inside a horizontal scroll region (0.4.9) | +``` + +Replace the `## Navbar/Logo: the centered title and version badge` section body (keep the `:::note` about `@theme-init` but reword its first sentence to "Wrapping a component from inside a theme package needs `@theme-init/`.") with: + +```markdown +## `Navbar/Content`, `Navbar/Logo` and `Navbar/MobileSidebar/PrimaryMenu`: the theme-owned navbar + +theme-classic renders `themeConfig.navbar.items`. This theme does not read +them. `Navbar/Content` renders one of two fixed layouts chosen from the site's +`baseUrl` (see [Exports](./exports.md#which-navbar-a-site-gets)), `Navbar/Logo` +renders the brand link with an href the site cannot change, and +`PrimaryMenu` repeats the developer navbar's external buttons in the mobile +drawer. The only input a site has is the `navbarLinks` option. + +`Navbar/Logo` reads `siteConfig.title` (developer variant only) and +`siteConfig.customFields.projectVersion`. theme-classic renders the title inside +the brand anchor, so the override hides it there with CSS and re-renders it as +its own absolutely centred element, which is what lets the version badge sit +next to it. A version without a leading `v` gets one added. + +The hamburger appears only on pages with a docs sidebar: with no +`themeConfig.navbar.items`, theme-common treats the drawer as empty otherwise. + +## `Navbar/SiteActions`: the public navbar's slot + +Renders `null`. The public navbar places it between search and the colour-mode +toggle. A site overrides it in its own `src/theme/Navbar/SiteActions/index.js` +to add a control of its own; the main docs site puts its Ask AI button there. +The developer navbar does not render the slot. + +## `Footer`: nothing + +Returns `null`. No Vantage documentation site has a footer: the sidebar's +collapse control already frames the bottom of the screen and the links a +footer would carry are the navbar buttons. Delete `themeConfig.footer` from +your config; leaving it in is harmless. +``` + +- [ ] **Step 3: `customization.md`** + +Replace the `## Overriding the logos` section with: + +```markdown +## Adding navbar buttons + +The developer navbar takes up to two external buttons through the theme option: + +```js +themes: [ + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, + {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, + ], + }], +], +``` + +That is the whole surface. The brand link, the title, the version badge and the +toggle are the theme's; `themeConfig.navbar` is not read. If a project needs a +third destination, put it in the docs, not the bar. + +## Adding a control to the public navbar + +The public navbar renders `@theme/Navbar/SiteActions`, which is empty. A site +on the public variant overrides it: + +``` +your-docs-site/ + src/ + theme/ + Navbar/ + SiteActions/ + index.js +``` + +```jsx +import AskAIButton from '@site/src/components/AskAIButton'; + +export default function NavbarSiteActions() { + return ; +} +``` +``` + +- [ ] **Step 4: `assets.md`** + +Change the row for `img/vantage-logo-color.svg` to: `| `img/vantage-logo-color.svg` | **Current.** What the theme's brand link renders. |` + +- [ ] **Step 5: Confirm no stale references remain in the reference docs** + +```bash +grep -rn "navbarLogo\|footerLogo\|ThemeLogo" docusaurus/docs/reference; echo "exit=$?" +``` + +Expected: no matches, `exit=1`. + +- [ ] **Step 6: Commit** + +```bash +git add docusaurus/docs/reference +git commit -m "docs: document navbarLinks, the navbar variants and the new overrides" +``` + +--- + +### Task 11: Usage, README and the migration guide + +**Files:** +- Modify: `docusaurus/docs/usage.md` +- Modify: `README.md` +- Modify: `MIGRATION.md` +- Modify: `docusaurus/docs/migration.md` + +- [ ] **Step 1: `usage.md`** + +In the config example: remove `navbarLogo,` and `footerLogo,` from the `require` destructure; change the `themes` line to the two-entry array from Task 10 Step 1 (labelled `// 2. Add the theme, with its one option.`); delete the `navbar: {...}` and `footer: {...}` objects from `themeConfig` and replace them with the comment `// No navbar and no footer here: the theme renders both. See Exports.`. Change the TypeScript import example to `import {staticDir, getProjectVersion}`. In the `### staticDirectories` paragraph, change "the brand mark that `navbarLogo` points at" to "the brand mark the navbar renders". + +- [ ] **Step 2: `README.md`** + +Apply the same four changes to the README's usage block (imports, `themes`, remove `navbar` and `footer`, comment). Then: + +- Delete the paragraph beginning "`navbarLogo` and `footerLogo` carry the Vantage brand mark". +- In "Brand Assets", change the brand-mark bullet's second sentence to "used by the theme's navbar." +- Replace the "Theme Component Overrides" table with the table from Task 10 Step 2. +- In the "Utilities" table, delete the `navbarLogo` and `footerLogo` rows and add: + +```markdown +| `resolveNavbarVariant(baseUrl)` | The rule that picks the public or developer navbar; exported for tooling | +``` + +- Replace the `### Overriding the logo` subsection with a `### Navbar buttons` subsection containing the `navbarLinks` snippet from Task 10 Step 3 and the sentence "That is the whole navbar surface a site has. See the docs site's Customization page for the public navbar's `SiteActions` slot." + +- [ ] **Step 3: `MIGRATION.md`, Part 4** + +Add a fourth bullet to the index at the top: + +```markdown +- **[Part 4: the theme-owned navbar](#part-4-the-theme-owned-navbar-050)** + is for a site on 0.4.x that declares its own `themeConfig.navbar` or + `footer`. Added in 0.5.0. +``` + +Append at the end of the file: + +```markdown +## Part 4: the theme-owned navbar (0.5.0) + +From 0.5.0 the theme renders the navbar itself and renders no footer. A site's +`themeConfig.navbar` and `themeConfig.footer` are ignored, and the +`navbarLogo`, `footerLogo` and `ThemeLogo` exports are gone. This is the +breaking change behind the minor bump. + +### Step 1: Upgrade the package + +```bash +npm install @vantagecompute/docusaurus-theme@^0.5.0 +``` + +### Step 2: Move your external buttons to the theme option + +```diff +- themes: ['@docusaurus/theme-mermaid', '@vantagecompute/docusaurus-theme'], ++ themes: [ ++ '@docusaurus/theme-mermaid', ++ ['@vantagecompute/docusaurus-theme', { ++ navbarLinks: [ ++ {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, ++ {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, ++ ], ++ }], ++ ], +``` + +Two at most, `label` and `url` only. Anything else in your old `items` (doc +links, dropdowns, a search item) has no equivalent; the developer navbar does +not carry them, by design. + +### Step 3: Delete the navbar and footer blocks + +```diff +- const {staticDir, navbarLogo, footerLogo, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); ++ const {staticDir, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); + + themeConfig: { +- navbar: { +- title: 'my-project', +- logo: navbarLogo, +- items: [...], +- }, +- footer: {...}, + prism: {...}, + }, +``` + +The centred title now comes from `siteConfig.title`, so make sure that is the +name you want beside the version badge. The version badge still reads +`customFields.projectVersion`. + +### Step 4: Delete any local navbar swizzle + +If your `src/theme/` has `Navbar/Content`, `Navbar/Logo`, +`Navbar/MobileSidebar/PrimaryMenu` or `Footer`, delete them; a local copy +silently wins over the theme's. + +### Step 5: Verify + +```bash +npm run build +``` + +Then open the site: the brand mark links to `/developer/` (or the docs root +on the main site), your buttons open in a new tab with the external-link icon, +and there is no footer. A misconfigured `navbarLinks` fails the build with a +message naming the entry. +``` + +- [ ] **Step 4: Mirror Part 4 into `docusaurus/docs/migration.md`** + +That file is the site copy of `MIGRATION.md` with a frontmatter block and lightly reworded headings (compare Parts 2 and 3 to see the house style: `### Step N: lowercase verb phrase`). Add the same index bullet and the same Part 4, with the step headings in that style. + +- [ ] **Step 5: Confirm no stale references remain anywhere** + +```bash +grep -rn "navbarLogo\|footerLogo\|ThemeLogo" README.md MIGRATION.md docusaurus/docs src; echo "exit=$?" +``` + +Expected: matches only inside `MIGRATION.md` and `docusaurus/docs/migration.md` (Parts 2 and 4 describe the old config on purpose). Nothing in `README.md`, `src/` or the other docs. + +- [ ] **Step 6: Build the docs site once more against the tarball** + +Repeat Task 8 Steps 4, 5 and 7 (pack, install `--no-save`, typecheck, build, assert, then `npm ci`). `onBrokenLinks: 'throw'` makes this the link check for the new anchors (`#which-navbar-a-site-gets`, `#part-4-the-theme-owned-navbar-050`). + +Expected: build succeeds, the assertions from Task 8 Step 5 hold, `git status` is clean apart from the intended edits. + +- [ ] **Step 7: Commit** + +```bash +git add README.md MIGRATION.md docusaurus/docs/usage.md docusaurus/docs/migration.md +git commit -m "docs: usage, README and migration for the theme-owned navbar" +``` + +--- + +### Task 12: Release (Bryan runs this after the PR merges) + +**Files:** none edited by hand; `just release` bumps `package.json`. + +- [ ] **Step 1: Open the PR** from this branch to `main` with the four assumptions from the top of this plan in the description, and a link to the decision record. CI must be green on both jobs. + +- [ ] **Step 2: Release 0.5.0** from a clean `main`: + +```bash +just release 0.5.0 +``` + +Expected: tag `v0.5.0` pushed, GitHub release created, the publish workflow runs `npm publish --provenance`. + +- [ ] **Step 3: Pin the docs site** + +```bash +just docs-pin 0.5.0 +``` + +Then commit `docusaurus/package.json` and `docusaurus/package-lock.json` (`chore(docs): track the theme at ^0.5.0`) and push. The deploy workflow rebuilds the spoke with the new navbar. + +- [ ] **Step 4: Hand off the vantage-docs work** + +Open the follow-up in vantage-docs: upgrade to `^0.5.0`, delete `themeConfig.navbar`, the logo and the `custom-askAI` item type, add `src/theme/Navbar/SiteActions/index.tsx` rendering `AskAIButton`, and remove the `Ctrl+I` `` from `AssistantLauncher.tsx` (decisions D5 and item 5 of the record). Spoke owners follow Part 4 of the migration guide. + +--- + +## Self-review + +**Spec coverage** +- D1 (logo to developer root inside `/developer/`, same tab): Task 1 `LOGO_HREF` + `resolveNavbarVariant`, Task 4 anchor with no `target`. +- D2 (two navbars, spokes declare none, developer shows name + version): Tasks 2, 3, 5; title fallback to `siteConfig.title` in Task 3. +- D3 (at most two `label`+`url` buttons, templated icon and rel, new tab): Task 1 validator, Task 3 `toNavbarItems`, Tasks 5 and 6 rendering. +- D4 (no footer anywhere): Task 7 override and CSS, Task 8 config, Task 10 docs. +- D5 (Ask AI hint): out of this repo; handed off in Task 12 Step 4. +- Confirmed choices 1 to 3: variant from `baseUrl` (Task 1), overrides plus options (Tasks 2, 5, 6), `SiteActions` slot (Task 5). + +**Type consistency** +- `ResolvedVantageThemeOptions.navbarLinks` (Task 1) is what `themeVantage` receives and republishes as `navbarLinks` in global data (Task 2); the hook renames it to `links` (Task 3) and both renderers use `links` (Tasks 5, 6). +- `toNavbarItems` and `useVantageNavbar` are exported by name from `src/theme/Navbar/useVantageNavbar.js` and imported with matching relative paths: `../useVantageNavbar` from `Navbar/Content` and `Navbar/Logo`, `../../useVantageNavbar` from `Navbar/MobileSidebar/PrimaryMenu`. +- `usePluginData('@vantagecompute/docusaurus-theme')` matches the plugin `name` in Task 2. + +**Known limits, stated rather than hidden** +- No React component tests exist in this repo; the component work is verified by building the docs site against the packed tarball (Task 8) in both variants. +- The docs site builds against npm in CI, so the PR's CI does not exercise the working-tree components. Task 8 and Task 11 Step 6 are the local gate. diff --git a/docusaurus/docs/migration.md b/docusaurus/docs/migration.md index 5fc189a..679869a 100644 --- a/docusaurus/docs/migration.md +++ b/docusaurus/docs/migration.md @@ -5,7 +5,7 @@ sidebar_position: 4 # Migration -Two migrations, depending on where a site starts. +Three migrations, depending on where a site starts. - **[Part 1](#part-1-adopting-the-theme)** is for a site not yet on the package: it still carries its own copy of the design system, the fonts and @@ -13,6 +13,9 @@ Two migrations, depending on where a site starts. - **[Part 2](#part-2-adopting-the-shared-brand-mark)** is for a site already on the package that still carries its own copy of the logo and its own navbar/footer logo blocks. Added in 0.4.7. +- **[Part 4](#part-4-the-theme-owned-navbar)** is for a site on 0.4.x that + still declares its own `themeConfig.navbar` or `footer`. Added in 0.5.0. + (Part 3, small screens, lives in the repository's `MIGRATION.md`.) ## Part 1: adopting the theme @@ -248,3 +251,73 @@ npm run serve - The navbar mark links to `https://docs.vantagecompute.ai` in the same tab - The footer mark links to `https://vantagecompute.ai` - No `static/img/vantage-logo-color.svg` remains in your repository + +## Part 4: the theme-owned navbar + +From 0.5.0 the theme renders the navbar itself and renders no footer. A site's +`themeConfig.navbar` and `themeConfig.footer` are ignored, and the +`navbarLogo`, `footerLogo` and `ThemeLogo` exports are gone. This is the +breaking change behind the minor bump. + +### Step 1: upgrade + +```bash +npm install @vantagecompute/docusaurus-theme@^0.5.0 +``` + +### Step 2: move your external buttons to the theme option + +```diff +- themes: ['@docusaurus/theme-mermaid', '@vantagecompute/docusaurus-theme'], ++ themes: [ ++ '@docusaurus/theme-mermaid', ++ ['@vantagecompute/docusaurus-theme', { ++ navbarLinks: [ ++ {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, ++ {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, ++ ], ++ }], ++ ], +``` + +Two at most, `label` and `url` only. Anything else in your old `items` (doc +links, dropdowns, a search item) has no equivalent; the developer navbar does +not carry them, by design. + +### Step 3: delete the navbar and footer blocks + +```diff +- const {staticDir, navbarLogo, footerLogo, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); ++ const {staticDir, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); + + themeConfig: { +- navbar: { +- title: 'my-project', +- logo: navbarLogo, +- items: [...], +- }, +- footer: {...}, + prism: {...}, + }, +``` + +The centred title now comes from `siteConfig.title`, so make sure that is the +name you want beside the version badge. The version badge still reads +`customFields.projectVersion`. + +### Step 4: delete any local navbar swizzle + +If your `src/theme/` has `Navbar/Content`, `Navbar/Logo`, +`Navbar/MobileSidebar/PrimaryMenu` or `Footer`, delete them; a local copy +silently wins over the theme's. + +### Step 5: verify + +```bash +npm run build +``` + +Then open the site: the brand mark links to `/developer/` (or the docs root +on the main site), your buttons open in a new tab with the external-link icon, +and there is no footer. A misconfigured `navbarLinks` fails the build with a +message naming the entry. diff --git a/docusaurus/docs/reference/assets.md b/docusaurus/docs/reference/assets.md index d8671f1..2193f3a 100644 --- a/docusaurus/docs/reference/assets.md +++ b/docusaurus/docs/reference/assets.md @@ -39,7 +39,7 @@ come from Google Fonts instead, so they are not in the package. | File | Status | |---|---| -| `img/vantage-logo-color.svg` | **Current.** What `navbarLogo` and `footerLogo` point at. | +| `img/vantage-logo-color.svg` | **Current.** What the theme's brand link renders. | | `img/vantage-logo.svg` | Legacy monochrome mark, kept for the `vantage-docs` hub, which still references it. New sites should not use it. | The colour mark has no dark variant on purpose: it is drawn to read in both diff --git a/docusaurus/docs/reference/components.md b/docusaurus/docs/reference/components.md index bea88c7..d52e81d 100644 --- a/docusaurus/docs/reference/components.md +++ b/docusaurus/docs/reference/components.md @@ -6,16 +6,20 @@ sidebar_position: 2 # Theme component overrides Adding the package to `themes` puts its `src/theme/` into the Docusaurus theme -resolution stack. These five overrides then take effect on every site, with no +resolution stack. These overrides then take effect on every site, with no per-site swizzling. | Component | What it changes | |---|---| +| `Navbar/Content` | The whole navbar, in a public or a developer variant chosen from `baseUrl` (0.5.0) | +| `Navbar/Logo` | The brand link with the variant's baked href, the centred title and the version badge | +| `Navbar/MobileSidebar/PrimaryMenu` | The developer navbar's external buttons, in the mobile drawer (0.5.0) | +| `Navbar/SiteActions` | An empty slot in the public navbar for a site's own controls (0.5.0) | +| `Navbar/MobileSidebar/SecondaryMenu` | A clean secondary-menu render | +| `Footer` | Renders nothing (0.5.0) | | `ColorModeToggle` | Sun and moon SVG icons in place of the default toggle | | `DocBreadcrumbs` | Full-path breadcrumbs instead of the truncated default | -| `Navbar/Logo` | A centered site title with a version badge beside it | | `Tabs` | A workaround for a Docusaurus 3.10 crash | -| `Navbar/MobileSidebar/SecondaryMenu` | A clean secondary-menu render | | `MDXComponents` | Every markdown `table` renders inside a horizontal scroll region (0.4.9) | ## `Tabs`: the one that is a bugfix @@ -40,27 +44,48 @@ this crashes the build. Revisit the override when the upstream fix lands; until then, removing it reintroduces the crash. -## `Navbar/Logo`: the centered title and version badge +## `Navbar/Content`, `Navbar/Logo` and `Navbar/MobileSidebar/PrimaryMenu`: the theme-owned navbar -Theme-classic renders the site title inside the brand anchor, beside the logo, -so centering the title in place would drag the logo to the middle with it. The -override instead hides the in-brand title with CSS and re-renders it as its own -absolutely centered element, which is what lets the version badge sit next to -it. +theme-classic renders `themeConfig.navbar.items`. This theme does not read +them. `Navbar/Content` renders one of two fixed layouts chosen from the site's +`baseUrl` (see [Exports](./exports.md#which-navbar-a-site-gets)), `Navbar/Logo` +renders the brand link with an href the site cannot change, and +`PrimaryMenu` repeats the developer navbar's external buttons in the mobile +drawer. The only input a site has is the `navbarLinks` option. -It reads `siteConfig.customFields.projectVersion` and -`siteConfig.themeConfig.navbar.title`. A missing version renders the title -alone. A version without a leading `v` gets one added; a version that already -has one is left as it is. +`Navbar/Logo` reads `siteConfig.title` (developer variant only) and +`siteConfig.customFields.projectVersion`. theme-classic renders the title inside +the brand anchor, so the override hides it there with CSS and re-renders it as +its own absolutely centred element, which is what lets the version badge sit +next to it. A version without a leading `v` gets one added. + +The hamburger appears only on pages with a docs sidebar, and only once the +page has hydrated: with no `themeConfig.navbar.items`, theme-common treats the +drawer as empty until the sidebar registers itself, which happens client-side. :::note `@theme-init`, not `@theme-original` -The override wraps the component below it with `@theme-init/Navbar/Logo`. -`@theme-original` would resolve back to this same component, because it ships -inside a theme package that is itself in the stack. React SSR then recurses -without bound and exhausts the heap during static site generation. If you -wrap a component from inside a theme package, use `@theme-init`. +Wrapping a component from inside a theme package needs `@theme-init/`. +`@theme-original` would resolve back to the wrapper itself, because the package +is in the stack it is resolving against. React SSR then recurses without bound +and exhausts the heap during static site generation. The navbar overrides above +replace their components outright and so sidestep this; `DocBreadcrumbs` and +`MDXComponents` wrap, and use `@theme-init`. ::: +## `Navbar/SiteActions`: the public navbar's slot + +Renders `null`. The public navbar places it between search and the colour-mode +toggle. A site overrides it in its own `src/theme/Navbar/SiteActions/index.js` +to add a control of its own; the main docs site puts its Ask AI button there. +The developer navbar does not render the slot. + +## `Footer`: nothing + +Returns `null`. No Vantage documentation site has a footer: the sidebar's +collapse control already frames the bottom of the screen and the links a +footer would carry are the navbar buttons. Delete `themeConfig.footer` from +your config; leaving it in is harmless. + ## `MDXComponents`: tables that scroll The theme frames tables with a border and a radius. Doing that on the table diff --git a/docusaurus/docs/reference/customization.md b/docusaurus/docs/reference/customization.md index ae3042b..ef59bd7 100644 --- a/docusaurus/docs/reference/customization.md +++ b/docusaurus/docs/reference/customization.md @@ -64,18 +64,46 @@ whatever it wraps, so it is the thing most likely to break on a Docusaurus upgrade, and the thing most likely to silently keep working while looking wrong. -## Overriding the logos +## Adding navbar buttons -`navbarLogo` and `footerLogo` are plain objects. Spread to change a field: +The developer navbar takes up to two external buttons through the theme option: ```js -navbar: { - logo: {...navbarLogo, href: 'https://docs.vantagecompute.ai/developer/'}, -}, +themes: [ + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, + {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, + ], + }], +], ``` -Spread rather than mutate: the exported objects are shared by every importer in -the process. Omit `logo` entirely to render none. +That is the whole surface. The brand link, the title, the version badge and the +toggle are the theme's; `themeConfig.navbar` is not read. If a project needs a +third destination, put it in the docs, not the bar. + +## Adding a control to the public navbar + +The public navbar renders `@theme/Navbar/SiteActions`, which is empty. A site +on the public variant overrides it: + +``` +your-docs-site/ + src/ + theme/ + Navbar/ + SiteActions/ + index.js +``` + +```jsx +import AskAIButton from '@site/src/components/AskAIButton'; + +export default function NavbarSiteActions() { + return ; +} +``` ## Keeping the search styling diff --git a/docusaurus/docs/reference/exports.md b/docusaurus/docs/reference/exports.md index fd45e32..2b56928 100644 --- a/docusaurus/docs/reference/exports.md +++ b/docusaurus/docs/reference/exports.md @@ -9,32 +9,41 @@ Everything the package exports from `@vantagecompute/docusaurus-theme`. | Export | Kind | Purpose | |---|---|---| -| `default` | Plugin factory | The theme itself. Goes in `themes`, not in `plugins`. | +| `default` | Plugin factory | The theme itself. Goes in `themes`, not in `plugins`. Takes the options below. | +| `validateOptions` | Plugin static | Docusaurus calls it; you never do. Rejects anything but `navbarLinks`. | | `staticDir` | `string` | Absolute path to the package's `static/`. Goes in `staticDirectories`. | | `getProjectVersion()` | `() => string` | The project version from git tags. | -| `navbarLogo` | `ThemeLogo` | Navbar logo config, linking to the docs hub. | -| `footerLogo` | `ThemeLogo` | Footer logo config, linking to the marketing site. | | `rehypeTabsTransform` | Rehype plugin | Lowercase ``/`` support in MDX. | -| `ThemeLogo` | `interface` | The shape of the two logo objects. | +| `resolveNavbarVariant(baseUrl)` | `(string) => 'public' \| 'developer'` | The rule the theme applies to pick a navbar. Exported for tests and tooling. | +| `LOGO_HREF` | `Record` | Where the brand mark links, per variant. | +| `MAX_NAVBAR_LINKS` | `number` | Two. | +| `VantageThemeOptions`, `NavbarLink`, `NavbarVariant` | types | The option shapes. | The entry point is CommonJS (`lib/index.cjs`) with type declarations, so both `require` and `import` work: ```js -const {staticDir, navbarLogo} = require('@vantagecompute/docusaurus-theme'); +const {staticDir, getProjectVersion} = require('@vantagecompute/docusaurus-theme'); ``` ```ts -import {staticDir, navbarLogo} from '@vantagecompute/docusaurus-theme'; +import {staticDir, getProjectVersion} from '@vantagecompute/docusaurus-theme'; ``` ## `default` (the theme) ```js -themes: ['@vantagecompute/docusaurus-theme'], +themes: [ + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/vantage-mcp'}, + {label: 'PyPI', url: 'https://pypi.org/project/vantage-mcp/'}, + ], + }], +], ``` -The factory returns a Docusaurus plugin that does three things: +The factory returns a Docusaurus plugin that does four things: - `getThemePath()` puts `src/theme/` into the theme resolution stack, which is what makes the [component overrides](./components.md) take effect. @@ -42,9 +51,40 @@ The factory returns a Docusaurus plugin that does three things: system loads. There is nothing to add to `customCss`. - `getPathsToWatch()` covers `src/theme/**/*.{js,jsx,ts,tsx,css}`, so a linked working tree hot-reloads during development. +- `contentLoaded()` publishes the navbar variant, the brand-link href and the + validated `navbarLinks` as plugin global data, which the navbar components + read. Nothing in `themeConfig.navbar` is read by the theme. + +### Options + +There is one. + +| Option | Type | Default | Meaning | +|---|---|---|---| +| `navbarLinks` | `{label: string, url: string}[]` | `[]` | External buttons on the right of the developer navbar. At most two. | + +Each entry is exactly `label` and `url`. The url must be absolute `http(s)`. +The theme adds the external-link icon, `target="_blank"` and +`rel="noopener noreferrer"`; a third entry, an extra property, a relative url +or an unknown option fails the build with a message naming the problem. + +The public navbar ignores `navbarLinks`; it has no buttons to add. + +### Which navbar a site gets -It takes no options. Everything configurable is configured through CSS tokens -and through your own `src/theme/` overrides. +The theme decides from `baseUrl`, and there is no override: + +| `baseUrl` | Variant | Brand link | Right-hand side | +|---|---|---|---| +| starts with `/developer/` | developer | `https://docs.vantagecompute.ai/developer/` | `navbarLinks` buttons, colour-mode toggle | +| anything else | public | `https://docs.vantagecompute.ai/` | search, the `Navbar/SiteActions` slot, colour-mode toggle | + +The developer navbar centres `siteConfig.title` with the version badge beside +it. The public navbar shows only the badge, when `customFields.projectVersion` +is set. + +Sites declare no `themeConfig.navbar` and no `themeConfig.footer`. A site that +still does builds fine; the theme renders neither. ## `staticDir` @@ -83,57 +123,6 @@ Two things to keep in mind: Pass the result through `customFields.projectVersion` for the navbar badge, and into the tagline if you want it on the page. Do not put it in `navbar.title`. -## `navbarLogo` and `footerLogo` - -```js -navbar: {title: 'my-project', logo: navbarLogo, items: [...]}, -footer: {style: 'dark', logo: footerLogo, links: [...]}, -``` - -Both carry the same brand mark, `img/vantage-logo-color.svg`, resolved out of -`staticDir`. They differ only in where they point: - -| | `href` | `target` | -|---|---|---| -| `navbarLogo` | `https://docs.vantagecompute.ai` | `_self` | -| `footerLogo` | `https://vantagecompute.ai` | default | - -The navbar mark points at the docs hub because from inside a project's -documentation the useful "home" is the rest of the documentation, and `_self` -so that jump replaces the tab. The footer mark points at the company, which is -what a reader who has finished reading is looking for. - -Neither has a `srcDark`, on purpose: the one colour mark is drawn to read in -both colour modes, and a second asset would only be a second thing to keep in -sync. - -**Override by spreading, never by mutating.** These are module-level objects -shared by everything that imports them in the process: - -```js -// Right -logo: {...navbarLogo, href: 'https://docs.vantagecompute.ai/developer/'}, - -// Wrong: changes the logo for every importer -navbarLogo.href = '...'; -``` - -Omit `logo` entirely to render no logo. - -### `ThemeLogo` - -```ts -interface ThemeLogo { - alt: string; - src: string; - srcDark?: string; - href: string; - target?: string; - width?: number; - height?: number; -} -``` - ## `rehypeTabsTransform` ```js diff --git a/docusaurus/docs/usage.md b/docusaurus/docs/usage.md index 9b57209..7a05a43 100644 --- a/docusaurus/docs/usage.md +++ b/docusaurus/docs/usage.md @@ -12,8 +12,6 @@ contributes is in the four highlighted places; the rest is ordinary Docusaurus. const { staticDir, rehypeTabsTransform, - navbarLogo, - footerLogo, getProjectVersion, } = require('@vantagecompute/docusaurus-theme'); @@ -25,8 +23,16 @@ const config = { // 1. The version goes in the tagline, never in `navbar.title`. tagline: `What this project does (${projectVersion})`, - // 2. Add the theme to the theme stack. - themes: ['@docusaurus/theme-mermaid', '@vantagecompute/docusaurus-theme'], + // 2. Add the theme, with its one option. + themes: [ + '@docusaurus/theme-mermaid', + ['@vantagecompute/docusaurus-theme', { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/my-project'}, + {label: 'PyPI', url: 'https://pypi.org/project/my-project/'}, + ], + }], + ], // 3. Serve the shared fonts, icons, brand mark and favicon. staticDirectories: ['static', staticDir], @@ -48,16 +54,8 @@ const config = { customFields: {projectVersion}, themeConfig: { - navbar: { - title: 'my-project', - logo: navbarLogo, - items: [/* ... */], - }, - footer: { - style: 'dark', - logo: footerLogo, - links: [/* ... */], - }, + // No navbar and no footer here: the theme renders both. See Exports. + prism: {/* ... */}, }, }; @@ -68,8 +66,7 @@ TypeScript sites use the same exports with `import`: ```ts import type {Config} from '@docusaurus/types'; -import {staticDir, navbarLogo, footerLogo, getProjectVersion} - from '@vantagecompute/docusaurus-theme'; +import {staticDir, getProjectVersion} from '@vantagecompute/docusaurus-theme'; ``` ## The four pieces, and why each is there @@ -87,8 +84,8 @@ order works. `staticDir` is the absolute path to the package's own `static/` directory. Listing it alongside your site's `static` means every asset the design system references resolves without your repository carrying a copy: the Satoshi -`@font-face` sources, the toggle and search icons, the brand mark that -`navbarLogo` points at, and `img/favicon.ico`. +`@font-face` sources, the toggle and search icons, the brand mark the navbar +renders, and `img/favicon.ico`. Leave it out and the CSS still loads, but the fonts fall back to Open Sans and the icons render as broken images. That failure is quiet, which is worth diff --git a/docusaurus/docusaurus.config.ts b/docusaurus/docusaurus.config.ts index cb23034..00a3e17 100644 --- a/docusaurus/docusaurus.config.ts +++ b/docusaurus/docusaurus.config.ts @@ -1,11 +1,6 @@ import type {Config} from '@docusaurus/types'; import {themes as prismThemes} from 'prism-react-renderer'; -import { - staticDir, - navbarLogo, - footerLogo, - getProjectVersion, -} from '@vantagecompute/docusaurus-theme'; +import {staticDir, getProjectVersion} from '@vantagecompute/docusaurus-theme'; // This site is the theme's own documentation, and it dogfoods the theme: the // dependency in package.json is the PUBLISHED package from npm, not a file: @@ -59,7 +54,22 @@ const config: Config = { hooks: {onBrokenMarkdownLinks: 'warn'}, }, - themes: ['@docusaurus/theme-mermaid', '@vantagecompute/docusaurus-theme'], + // This site is a developer spoke (baseUrl under /developer/), so the theme + // renders its developer navbar: brand mark, centred title and version, and + // these two buttons. There is no themeConfig.navbar and no footer; both are + // the theme's, not the site's. See docs/reference/exports.md. + themes: [ + '@docusaurus/theme-mermaid', + [ + '@vantagecompute/docusaurus-theme', + { + navbarLinks: [ + {label: 'GitHub', url: 'https://github.com/vantagecompute/vantage-docusaurus-theme'}, + {label: 'npm', url: 'https://www.npmjs.com/package/@vantagecompute/docusaurus-theme'}, + ], + }, + ], + ], // `staticDir` serves the theme's own fonts, icons, brand mark and favicon. // This site needs it for the same reason every other Vantage site does, and @@ -118,64 +128,6 @@ const config: Config = { customFields: {projectVersion}, themeConfig: { - navbar: { - title: 'docusaurus-theme', - logo: navbarLogo, - items: [ - {type: 'docSidebar', sidebarId: 'docsSidebar', position: 'left', label: 'Docs'}, - {to: '/reference/exports', label: 'API', position: 'left'}, - { - href: 'https://www.npmjs.com/package/@vantagecompute/docusaurus-theme', - label: 'npm', - position: 'right', - }, - { - href: 'https://github.com/vantagecompute/vantage-docusaurus-theme', - label: 'GitHub', - position: 'right', - className: 'github-button', - }, - ], - }, - footer: { - style: 'dark', - logo: footerLogo, - links: [ - { - title: 'Documentation', - items: [ - {label: 'Overview', to: '/'}, - {label: 'Installation', to: '/installation'}, - {label: 'Usage', to: '/usage'}, - {label: 'Migration', to: '/migration'}, - ], - }, - { - title: 'Reference', - items: [ - {label: 'Exports', to: '/reference/exports'}, - {label: 'Components', to: '/reference/components'}, - {label: 'Assets', to: '/reference/assets'}, - {label: 'Design tokens', to: '/design-system/tokens'}, - ], - }, - { - title: 'More', - items: [ - { - label: 'GitHub', - href: 'https://github.com/vantagecompute/vantage-docusaurus-theme', - }, - { - label: 'npm', - href: 'https://www.npmjs.com/package/@vantagecompute/docusaurus-theme', - }, - {label: 'Vantage Compute', href: 'https://vantagecompute.ai'}, - ], - }, - ], - copyright: `Copyright © ${new Date().getFullYear()} Vantage Compute.`, - }, codeBlock: {showCopyButton: true}, prism: { theme: prismThemes.vsLight, diff --git a/package.json b/package.json index 2a6630a..7fe2aa6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "scripts": { "build": "tsc", + "test": "node --test 'test/**/*.test.cjs'", "prepublishOnly": "npm run build" }, "peerDependencies": { diff --git a/src/css/custom.css b/src/css/custom.css index ec4e17f..ed6e301 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -849,12 +849,6 @@ html[data-theme='dark'] .provider-showcase-item { * --vantage-navbar-logo-hide-bp Breakpoint below which logo hides * --vantage-navbar-brand-pl Left padding on the brand area * --vantage-navbar-items-gap Gap between right-side items - * --vantage-footer-bg Footer background colour - * --vantage-footer-text Footer text colour - * --vantage-footer-title-color Footer section-title colour - * --vantage-footer-link-hover Footer link hover colour - * --vantage-footer-separator Footer separator colour - * --vantage-footer-logo-max-h Footer logo max-height */ :root { --ifm-navbar-height: 64px; @@ -869,14 +863,6 @@ html[data-theme='dark'] .provider-showcase-item { --vantage-navbar-logo-hide-bp: 1024px; --vantage-navbar-brand-pl: 6px; --vantage-navbar-items-gap: 10px; - - /* Footer defaults */ - --vantage-footer-bg: var(--iris-900); - --vantage-footer-text: #ffffff; - --vantage-footer-title-color: var(--iris-200); - --vantage-footer-link-hover: var(--iris-200); - --vantage-footer-separator: rgba(255, 255, 255, 0.12); - --vantage-footer-logo-max-h: 60px; } .navbar { @@ -1385,93 +1371,6 @@ details summary[onclick]:hover { text-decoration: underline !important; } -/* ── Footer ────────────────────────────────────────────────────────── */ -.footer { - background-color: var(--vantage-footer-bg) !important; - border-top: 1px solid var(--vantage-navbar-border); -} - -.footer--dark { - background-color: var(--vantage-footer-bg) !important; -} - -.footer__title, -.footer__item, -.footer__link-item, -.footer__copyright { - color: var(--vantage-footer-text) !important; -} - -.footer__title { - font-size: 12px; - text-transform: uppercase; - letter-spacing: 0.08em; - font-weight: 700; - color: var(--vantage-footer-title-color) !important; -} - -.footer__link-item:hover { - color: var(--vantage-footer-link-hover) !important; -} - -.footer__separator { - background-color: var(--vantage-footer-separator) !important; -} - -.footer__links { - justify-content: center !important; -} - -.footer__col { - text-align: center !important; -} - -.footer__logo { - height: auto !important; - width: auto !important; - max-height: var(--vantage-footer-logo-max-h); -} - -.footer__link-social { - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - width: 40px !important; - height: 40px !important; - margin: 0 8px 8px 0 !important; - padding: 8px !important; - border-radius: 8px !important; - background-color: rgba(255, 255, 255, 0.1) !important; - transition: all 0.3s ease !important; - text-decoration: none !important; -} - -.footer__link-social:hover { - background-color: rgba(255, 255, 255, 0.2) !important; - transform: translateY(-2px) !important; - text-decoration: none !important; -} - -.footer__link-social img { - width: 24px !important; - height: 24px !important; - filter: brightness(0) invert(1) !important; - transition: all 0.3s ease !important; -} - -.footer__link-social:hover img { - filter: brightness(0) invert(1) !important; - transform: scale(1.1) !important; -} - -[data-theme='dark'] .footer__link-social img { - filter: brightness(0) invert(1) !important; -} - -[data-theme='light'] .footer__link-social img { - filter: brightness(0) invert(1) !important; -} - /* ── DocSearch (Algolia) - full theme ──────────────────────────────── */ :root { /* Core */ diff --git a/src/index.cts b/src/index.cts index 6500786..4b0f1ca 100644 --- a/src/index.cts +++ b/src/index.cts @@ -1,8 +1,38 @@ import path from 'node:path'; -import { execSync } from 'node:child_process'; -import type { Plugin } from '@docusaurus/types'; +import {execSync} from 'node:child_process'; +import type {LoadContext, OptionValidationContext, Plugin} from '@docusaurus/types'; +import { + LOGO_HREF, + resolveNavbarVariant, + validateVantageThemeOptions, + type ResolvedVantageThemeOptions, + type VantageThemeOptions, +} from './options.cjs'; + +export type {NavbarLink, NavbarVariant, VantageThemeOptions} from './options.cjs'; +export {LOGO_HREF, MAX_NAVBAR_LINKS, resolveNavbarVariant} from './options.cjs'; + +/** + * Shape of the global data the theme's client components read through + * `usePluginData('@vantagecompute/docusaurus-theme')`. + */ +export interface VantageThemeGlobalData { + variant: 'public' | 'developer'; + logoHref: string; + navbarLinks: ResolvedVantageThemeOptions['navbarLinks']; +} + +export default function themeVantage( + context: LoadContext, + options: ResolvedVantageThemeOptions, +): Plugin { + const variant = resolveNavbarVariant(context.baseUrl); + const globalData: VantageThemeGlobalData = { + variant, + logoHref: LOGO_HREF[variant], + navbarLinks: options.navbarLinks, + }; -export default function themeVantage(): Plugin { return { name: '@vantagecompute/docusaurus-theme', @@ -17,9 +47,26 @@ export default function themeVantage(): Plugin { getClientModules() { return [path.resolve(__dirname, '../src/css/custom.css')]; }, + + // The navbar variant and the external buttons reach the client this way. + // Nothing in themeConfig.navbar is read by the theme's components. + contentLoaded({actions}) { + actions.setGlobalData(globalData); + }, }; } +/** + * Docusaurus calls this before the plugin factory. Hand-rolled rather than Joi + * so the package carries no validation dependency; see src/options.cts. + */ +export function validateOptions({ + options, +}: OptionValidationContext): + ResolvedVantageThemeOptions { + return validateVantageThemeOptions(options); +} + /** * Returns the absolute path to this package's static directory. * Add this to your `staticDirectories` in docusaurus.config.js: @@ -52,74 +99,3 @@ export function getProjectVersion(): string { // Re-export the rehype utility (plain JS, lives in src/utils/) export const rehypeTabsTransform = require(path.resolve(__dirname, '../src/utils/rehypeTabsTransform')); - -/** - * Shape of a Docusaurus navbar or footer logo entry. - */ -export interface ThemeLogo { - alt: string; - src: string; - srcDark?: string; - href: string; - target?: string; - width?: number; - height?: number; -} - -/** - * The Vantage colour brand mark, as a path relative to a served static - * directory. It resolves once `staticDir` is in `staticDirectories`; no site - * needs its own copy of the SVG. - */ -const VANTAGE_LOGO_SRC = 'img/vantage-logo-color.svg'; - -/** - * Navbar logo for a Vantage documentation site. - * - * ```js - * const { navbarLogo } = require('@vantagecompute/docusaurus-theme'); - * themeConfig: { navbar: { title: 'v8x', logo: navbarLogo, items: [...] } } - * ``` - * - * Deliberately has no `srcDark`. The single colour mark is drawn to read on - * both colour modes, and a second asset would only be a second thing to keep - * in sync. - * - * `href` points at the docs hub rather than the marketing site: from a spoke's - * documentation the useful "home" is the rest of the documentation. `target` - * is `_self` so that jump replaces the tab instead of opening a new one. - * - * Override by spreading, never by mutating -- the object is shared by every - * site in the process: - * - * ```js - * logo: { ...navbarLogo, href: 'https://docs.vantagecompute.ai/developer/' } - * ``` - * - * Omit `logo` entirely to render no navbar logo. - */ -export const navbarLogo: ThemeLogo = { - alt: 'Vantage Compute Logo', - src: VANTAGE_LOGO_SRC, - href: 'https://docs.vantagecompute.ai', - target: '_self', -}; - -/** - * Footer logo for a Vantage documentation site. - * - * ```js - * const { footerLogo } = require('@vantagecompute/docusaurus-theme'); - * themeConfig: { footer: { style: 'dark', logo: footerLogo, links: [...] } } - * ``` - * - * Same mark as {@link navbarLogo}, but `href` points at the marketing site: - * the footer is where a reader who has finished reading looks for the company. - * - * Override by spreading; omit `logo` to render no footer logo. - */ -export const footerLogo: ThemeLogo = { - alt: 'Vantage Compute Logo', - src: VANTAGE_LOGO_SRC, - href: 'https://vantagecompute.ai', -}; diff --git a/src/options.cts b/src/options.cts new file mode 100644 index 0000000..ed16d3e --- /dev/null +++ b/src/options.cts @@ -0,0 +1,143 @@ +/** + * Theme options and the navbar variant rule. Pure functions with no Docusaurus + * imports, so they are tested directly against the compiled output. + */ + +export type NavbarVariant = 'public' | 'developer'; + +/** One external button in the developer navbar. Nothing else is configurable. */ +export interface NavbarLink { + label: string; + url: string; +} + +/** What a site may pass in `themes: [['@vantagecompute/docusaurus-theme', {...}]]`. */ +export interface VantageThemeOptions { + /** External buttons, at most {@link MAX_NAVBAR_LINKS}. Rendered by the developer navbar only. */ + navbarLinks?: NavbarLink[]; +} + +/** + * Options after validation. `id` is Docusaurus's plugin-instance id. When a + * plugin exports validateOptions, Docusaurus trusts the returned object to + * carry it (its own Joi path adds one), and names the plugin's generated data + * directory after it. Leaving it out crashes the build in path.join. + */ +export interface ResolvedVantageThemeOptions { + id: string; + navbarLinks: NavbarLink[]; +} + +const DEFAULT_PLUGIN_ID = 'default'; + +/** Two is GitHub plus one registry (PyPI, npm, ...). More than that is a nav, and navs drift. */ +export const MAX_NAVBAR_LINKS = 2; + +/** + * Where the brand mark links, per variant. Baked in on purpose: a site cannot + * point its logo anywhere else, which is what keeps the hub consistent. + */ +export const LOGO_HREF: Record = { + public: 'https://docs.vantagecompute.ai/', + developer: 'https://docs.vantagecompute.ai/developer/', +}; + +/** + * Every site published under /developer/ (the developer overview and every + * spoke) gets the developer navbar. Everything else gets the public one. + * Docusaurus normalises baseUrl to a leading and trailing slash before the + * plugin sees it. + */ +export function resolveNavbarVariant(baseUrl: string): NavbarVariant { + return baseUrl.startsWith('/developer/') ? 'developer' : 'public'; +} + +const PREFIX = '[@vantagecompute/docusaurus-theme]'; + +function fail(message: string): never { + throw new Error(`${PREFIX} ${message}`); +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function validateLink(value: unknown, index: number): NavbarLink { + const where = `navbarLinks[${index}]`; + if (!isPlainObject(value)) { + fail(`${where} must be an object with "label" and "url".`); + } + + const extra = Object.keys(value).filter((key) => key !== 'label' && key !== 'url'); + if (extra.length > 0) { + fail( + `${where} has unsupported propert${extra.length === 1 ? 'y' : 'ies'} ` + + `${extra.map((k) => `"${k}"`).join(', ')}. Only "label" and "url" are ` + + `accepted; the icon, external-link marker and rel attributes come from the theme.`, + ); + } + + const {label, url} = value; + if (typeof label !== 'string' || label.trim() === '') { + fail(`${where}.label must be a non-empty string.`); + } + if (typeof url !== 'string' || url.trim() === '') { + fail(`${where}.url must be a non-empty string.`); + } + + let parsed: URL; + try { + parsed = new URL(url); + } catch { + return fail(`${where}.url must be an absolute http(s) URL, got "${url}".`); + } + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + fail(`${where}.url must be an absolute http(s) URL, got "${url}".`); + } + + return {label: label.trim(), url}; +} + +/** + * Validate and normalise the theme options. Throws with a message that names + * the offending key, so a misconfigured spoke fails its build instead of + * quietly rendering something off-convention. + */ +export function validateVantageThemeOptions(options: unknown): ResolvedVantageThemeOptions { + if (options === undefined || options === null) { + return {id: DEFAULT_PLUGIN_ID, navbarLinks: []}; + } + if (!isPlainObject(options)) { + fail('theme options must be an object.'); + } + + const {navbarLinks, id, ...unknown} = options; + const unknownKeys = Object.keys(unknown); + if (unknownKeys.length > 0) { + fail( + `unknown option${unknownKeys.length === 1 ? '' : 's'} ` + + `${unknownKeys.map((k) => `"${k}"`).join(', ')}. The only option is "navbarLinks".`, + ); + } + + const resolved: ResolvedVantageThemeOptions = { + id: typeof id === 'string' ? id : DEFAULT_PLUGIN_ID, + navbarLinks: [], + }; + + if (navbarLinks === undefined) { + return resolved; + } + if (!Array.isArray(navbarLinks)) { + fail('"navbarLinks" must be an array of {label, url} objects.'); + } + if (navbarLinks.length > MAX_NAVBAR_LINKS) { + fail( + `"navbarLinks" allows at most ${MAX_NAVBAR_LINKS} entries, got ${navbarLinks.length}. ` + + `GitHub plus one package registry is the intended use.`, + ); + } + + resolved.navbarLinks = navbarLinks.map(validateLink); + return resolved; +} diff --git a/src/theme/Footer/index.js b/src/theme/Footer/index.js new file mode 100644 index 0000000..6179510 --- /dev/null +++ b/src/theme/Footer/index.js @@ -0,0 +1,9 @@ +/** + * No Vantage documentation site renders a footer. The collapse-sidebar control + * already frames the bottom of the screen, and the links a footer would carry + * are the navbar's external buttons. A site that still declares + * themeConfig.footer builds fine and renders nothing. + */ +export default function Footer() { + return null; +} diff --git a/src/theme/Navbar/Content/index.jsx b/src/theme/Navbar/Content/index.jsx new file mode 100644 index 0000000..170a339 --- /dev/null +++ b/src/theme/Navbar/Content/index.jsx @@ -0,0 +1,91 @@ +import React from 'react'; +import clsx from 'clsx'; +import {ErrorCauseBoundary, ThemeClassNames} from '@docusaurus/theme-common'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import NavbarItem from '@theme/NavbarItem'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import SearchBar from '@theme/SearchBar'; +import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; +import NavbarLogo from '@theme/Navbar/Logo'; +import NavbarSearch from '@theme/Navbar/Search'; +import NavbarSiteActions from '@theme/Navbar/SiteActions'; +import {toNavbarItems, useVantageNavbar} from '../useVantageNavbar'; + +import styles from './styles.module.css'; + +/** + * The navbar, replacing theme-classic's Navbar/Content. + * + * Two fixed variants, chosen by the theme from the site's baseUrl: + * + * public logo | search, SiteActions slot, colour-mode toggle + * developer logo + centred title and version | external buttons, toggle + * + * themeConfig.navbar.items is never read. A site that still declares items + * gets no error and no rendering; the convention is the theme's to hold. + */ +function ExternalLinks({items}) { + return ( + <> + {items.map((item, i) => ( + + new Error( + `A theme navbar link failed to render: ${JSON.stringify(item)}`, + {cause: error}, + ) + }> + + + ))} + + ); +} + +function NavbarContentLayout({left, right}) { + return ( +
+
+ {left} +
+
+ {right} +
+
+ ); +} + +export default function NavbarContent() { + const mobileSidebar = useNavbarMobileSidebar(); + const {variant, links} = useVantageNavbar(); + + const left = ( + <> + {!mobileSidebar.disabled && } + + + ); + + const right = + variant === 'developer' ? ( + <> + + + + ) : ( + <> + + + + + + + ); + + return ; +} diff --git a/src/theme/Navbar/Content/styles.module.css b/src/theme/Navbar/Content/styles.module.css new file mode 100644 index 0000000..514fb74 --- /dev/null +++ b/src/theme/Navbar/Content/styles.module.css @@ -0,0 +1,6 @@ +/* The mobile drawer header has its own toggle. */ +@media (max-width: 996px) { + .colorModeToggle { + display: none; + } +} diff --git a/src/theme/Navbar/Logo/index.jsx b/src/theme/Navbar/Logo/index.jsx index e64532b..fb16f5d 100644 --- a/src/theme/Navbar/Logo/index.jsx +++ b/src/theme/Navbar/Logo/index.jsx @@ -1,36 +1,39 @@ import React from 'react'; -// Must be @theme-init, NOT @theme-original. This component ships inside a -// theme package that sits in the theme stack, so @theme-original/Navbar/Logo -// resolves back to this same component -- React SSR then recurses without -// bound and exhausts the heap during static site generation. @theme-init is -// the alias for a theme wrapping the implementation below it in the stack. -import Logo from '@theme-init/Navbar/Logo'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import {useVantageNavbar} from '../useVantageNavbar'; + +// The one colour mark reads on both colour modes, so there is no srcDark and +// no second asset to keep in sync. Resolves through staticDir, which every +// Vantage site lists in staticDirectories. +const LOGO_SRC = 'img/vantage-logo-color.svg'; /** - * Brand logo on the left, plus a centered title with the project version - * immediately to its right. + * The brand link, the centred title and the version badge. + * + * Replaces theme-classic's Logo outright instead of wrapping it, because the + * href is not the site's to choose: the theme bakes it per variant (the docs + * root for the public navbar, the developer overview for the developer one). + * A plain anchor rather than @docusaurus/Link because the jump crosses SPA + * boundaries and should be a full navigation. Same tab: command-click covers + * "open in a new tab". * - * The title cannot just be centered in place: theme-classic renders it inside - * the brand , beside the logo, so centering it would drag the logo along. - * Instead the in-brand title is hidden by CSS and re-rendered here as its own - * absolutely-centered element, which lets the version badge sit next to it. + * The markup mirrors theme-classic's (navbar__brand > navbar__logo > img, + * plus b.navbar__title) so Infima's and this theme's CSS keep applying. On + * desktop the in-brand title is hidden by CSS and re-rendered centred below, + * which is what lets the version badge sit beside it. */ -export default function LogoWrapper(props) { - const {siteConfig} = useDocusaurusContext(); - - const raw = siteConfig.customFields?.projectVersion; - const version = raw - ? String(raw).startsWith('v') - ? String(raw) - : `v${raw}` - : null; - - const title = siteConfig.themeConfig?.navbar?.title; +export default function NavbarLogo() { + const {logoHref, title, version} = useVantageNavbar(); + const src = useBaseUrl(LOGO_SRC); return ( <> - + +
+ Vantage Compute Logo +
+ {title && {title}} +
{(title || version) && (
{title && {title}} diff --git a/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.jsx b/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.jsx new file mode 100644 index 0000000..bcae126 --- /dev/null +++ b/src/theme/Navbar/MobileSidebar/PrimaryMenu/index.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import NavbarItem from '@theme/NavbarItem'; +import {toNavbarItems, useVantageNavbar} from '../../useVantageNavbar'; + +/** + * The primary panel of the mobile drawer. theme-classic fills it from + * themeConfig.navbar.items; this theme fills it from the same external + * buttons the developer navbar shows. The public navbar has nothing to put + * here (search and the site's actions stay in the bar), so the panel is empty + * and the drawer opens straight onto the docs sidebar. + */ +export default function NavbarMobilePrimaryMenu() { + const mobileSidebar = useNavbarMobileSidebar(); + const {variant, links} = useVantageNavbar(); + + if (variant !== 'developer' || links.length === 0) { + return null; + } + + return ( +
    + {toNavbarItems(links).map((item, i) => ( + mobileSidebar.toggle()} key={i} /> + ))} +
+ ); +} diff --git a/src/theme/Navbar/SiteActions/index.jsx b/src/theme/Navbar/SiteActions/index.jsx new file mode 100644 index 0000000..888bf43 --- /dev/null +++ b/src/theme/Navbar/SiteActions/index.jsx @@ -0,0 +1,11 @@ +/** + * A slot in the public navbar, between search and the colour-mode toggle. + * Empty here. The main docs site overrides it in its own src/theme/ to render + * its Ask AI button; that component talks to the Vantage AI backend and does + * not belong in a theme package. + * + * The developer navbar does not render this slot. + */ +export default function NavbarSiteActions() { + return null; +} diff --git a/src/theme/Navbar/useVantageNavbar.js b/src/theme/Navbar/useVantageNavbar.js new file mode 100644 index 0000000..e3082bf --- /dev/null +++ b/src/theme/Navbar/useVantageNavbar.js @@ -0,0 +1,48 @@ +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import {usePluginData} from '@docusaurus/useGlobalData'; + +/** + * Everything the theme's navbar components need, in one place. The variant, + * the brand-link href and the external buttons come from the theme's Node + * side (setGlobalData in lib/index.cjs). The title and version come from the + * site config. themeConfig.navbar is deliberately never read. + */ +export function useVantageNavbar() { + const {siteConfig} = useDocusaurusContext(); + const {variant, logoHref, navbarLinks} = usePluginData('@vantagecompute/docusaurus-theme'); + + const raw = siteConfig.customFields?.projectVersion; + const version = raw + ? String(raw).startsWith('v') + ? String(raw) + : `v${raw}` + : null; + + return { + variant, + logoHref, + links: navbarLinks, + // The developer navbar names the project beside its version, which is + // how a reader confirms they are on the version they think they are. + // The public navbar is logo-only. + title: variant === 'developer' ? siteConfig.title : null, + version, + }; +} + +/** + * Map the validated {label, url} links onto theme-classic NavbarItem props. + * External destinations open in a new tab, the accepted convention for + * leaving a site. NavbarNavLink appends the external-link icon on its own + * because href is external and label is set. + */ +export function toNavbarItems(links) { + return links.map((link) => ({ + label: link.label, + href: link.url, + position: 'right', + target: '_blank', + rel: 'noopener noreferrer', + className: 'navbar__external-link', + })); +} diff --git a/test/options.test.cjs b/test/options.test.cjs new file mode 100644 index 0000000..a1f7612 --- /dev/null +++ b/test/options.test.cjs @@ -0,0 +1,115 @@ +// Runs against the compiled output, the same code consumers get. Build first: +// yarn build && yarn test +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + validateVantageThemeOptions, + resolveNavbarVariant, + LOGO_HREF, + MAX_NAVBAR_LINKS, +} = require('../lib/options.cjs'); + +const github = {label: 'GitHub', url: 'https://github.com/vantagecompute/vantage-mcp'}; +const pypi = {label: 'PyPI', url: 'https://pypi.org/project/vantage-mcp/'}; +const npm = {label: 'npm', url: 'https://www.npmjs.com/package/vantage-mcp'}; + +// Docusaurus relies on validateOptions to return an `id`: its own Joi path +// adds one, and the plugin's data directory is named after it. Without it the +// build dies in path.join with "Received undefined". +test('no options at all resolves to no links and the default id', () => { + assert.deepEqual(validateVantageThemeOptions(undefined), {id: 'default', navbarLinks: []}); + assert.deepEqual(validateVantageThemeOptions({}), {id: 'default', navbarLinks: []}); +}); + +test('one and two links pass through normalised', () => { + assert.deepEqual(validateVantageThemeOptions({navbarLinks: [github]}), { + id: 'default', + navbarLinks: [github], + }); + assert.deepEqual(validateVantageThemeOptions({navbarLinks: [github, pypi]}), { + id: 'default', + navbarLinks: [github, pypi], + }); +}); + +test('labels are trimmed', () => { + const out = validateVantageThemeOptions({navbarLinks: [{...github, label: ' GitHub '}]}); + assert.equal(out.navbarLinks[0].label, 'GitHub'); +}); + +test('a third link is rejected', () => { + assert.equal(MAX_NAVBAR_LINKS, 2); + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [github, pypi, npm]}), + /at most 2/, + ); +}); + +test('a link with an extra property is rejected', () => { + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{...github, icon: 'github'}]}), + /"icon"/, + ); +}); + +test('a link needs a non-empty label', () => { + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{url: github.url}]}), + /label/, + ); + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{label: ' ', url: github.url}]}), + /label/, + ); +}); + +test('a link needs an absolute http(s) url', () => { + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{label: 'Docs', url: '/docs'}]}), + /url/, + ); + assert.throws( + () => validateVantageThemeOptions({navbarLinks: [{label: 'Mail', url: 'mailto:x@y.z'}]}), + /url/, + ); +}); + +test('navbarLinks must be an array', () => { + assert.throws(() => validateVantageThemeOptions({navbarLinks: github}), /array/); +}); + +test('an unknown top-level option is rejected by name', () => { + assert.throws(() => validateVantageThemeOptions({logoUrl: '/'}), /"logoUrl"/); +}); + +test('options must be an object', () => { + assert.throws(() => validateVantageThemeOptions([github]), /object/); + assert.throws(() => validateVantageThemeOptions('nope'), /object/); +}); + +test('an explicit Docusaurus plugin id passes through untouched', () => { + assert.deepEqual(validateVantageThemeOptions({id: 'second', navbarLinks: [github]}), { + id: 'second', + navbarLinks: [github], + }); +}); + +test('baseUrl under /developer/ is the developer variant', () => { + assert.equal(resolveNavbarVariant('/developer/'), 'developer'); + assert.equal(resolveNavbarVariant('/developer/vantage-mcp/'), 'developer'); + assert.equal(resolveNavbarVariant('/developer/docusaurus-theme/'), 'developer'); +}); + +test('every other baseUrl is the public variant', () => { + assert.equal(resolveNavbarVariant('/'), 'public'); + assert.equal(resolveNavbarVariant('/docs/'), 'public'); + assert.equal(resolveNavbarVariant('/developers/'), 'public'); +}); + +test('logo hrefs are baked per variant', () => { + assert.deepEqual(LOGO_HREF, { + public: 'https://docs.vantagecompute.ai/', + developer: 'https://docs.vantagecompute.ai/developer/', + }); +});