diff --git a/.github/screenshots/dark.png b/.github/screenshots/dark.png index 72ae782..192b660 100644 Binary files a/.github/screenshots/dark.png and b/.github/screenshots/dark.png differ diff --git a/.github/screenshots/light.png b/.github/screenshots/light.png index e2025cc..5d0b59f 100644 Binary files a/.github/screenshots/light.png and b/.github/screenshots/light.png differ diff --git a/.gitignore b/.gitignore index 422d090..25068ac 100644 --- a/.gitignore +++ b/.gitignore @@ -125,6 +125,9 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +# Editor/AI assistant local config +.cursor/ + # pnpm .pnpm-store diff --git a/README.md b/README.md index 253e4ed..2c1acb1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ export default defineConfig({ ``` That's it - the plugin wires in the theme CSS, self-hosted fonts, the branded -`SiteTitle`/`Head`/`Footer` components, and a default GitHub social link. +header, splash footer, and a default GitHub social link. Pass `nav` if you want +top-level Docs / Guides / Reference tabs. ### Serving under a subpath @@ -106,9 +107,12 @@ only needs `light`. - **Typography** - Geist for body and headings, IBM Plex Mono for code, both self-hosted (no external font requests at runtime). - **Logo, favicon, and footer** - the Nebari mark in the header, an inlined - symbol favicon, and a branded footer on every page. -- **Search** - Starlight's built-in Pagefind, ready to merge additional pack - indexes for portal-wide multisite search. + symbol favicon, and a branded multi-column footer on splash pages (home, 404). + Doc pages with a sidebar end at their content. +- **Nav tabs** - optional `nav` items render as header tabs and in the mobile + drawer. Omit the option and the header stays stock Starlight. +- **Search** - Starlight's built-in Pagefind, styled to the Docs theme, ready to + merge additional pack indexes for portal-wide multisite search. Everything is overridable: your own `customCss`, `components`, and `social` entries are merged after the theme's, so a consumer always wins. @@ -146,6 +150,19 @@ Run the test suite (builds the package and docs, then runs the tests): bun test ``` +The root `test` script covers `packages/starlight/test` and `docs/test` (the +`--base /demo-pack` build). End-to-end checks live in `docs/`: + +```sh +cd docs && bun run e2e +``` + +Regenerate the README screenshots (out of CI, 1440×900): + +```sh +cd docs && bun run screenshots +``` + ### Linting and formatting [Biome](https://biomejs.dev) handles both formatting and linting (including @@ -196,9 +213,20 @@ This package follows [EffVer](https://jacobtomlinson.dev/effver/) your build unexpectedly: ```jsonc -{ "dependencies": { "@nebari/starlight": "^0.1.0" } } +{ "dependencies": { "@nebari/starlight": "^0.3.0" } } ``` +### 0.3.0 + +EffVer meso. Four behaviours change without an opt-in: + +1. The footer no longer renders on doc pages — splash pages only. +2. `lastUpdated` defaults to `true`, so a date appears on pages that had none. +3. Nav tabs appear only if `nav` is set; absent, the header is byte-identical. +4. Tables scroll rather than wrap. Long values scroll inside the cell or the + table instead of breaking across lines. With JavaScript disabled, a wide + table can overflow the page. + ## Releasing Releases are published to npm automatically from GitHub Releases via diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 0434e0c..e9df874 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -11,7 +11,7 @@ export default defineConfig({ nebari({ nav: [ { label: 'Docs', href: '/' }, - { label: 'Guides', href: '/guides/authoring-content/' }, + { label: 'Guides', href: '/guides/' }, { label: 'Reference', href: '/reference/configuration/' }, ], }), @@ -32,6 +32,7 @@ export default defineConfig({ { label: 'Guides', items: [ + { label: 'Overview', link: '/guides/' }, { label: 'Authoring Content', link: '/guides/authoring-content/', diff --git a/docs/e2e/screenshots.spec.ts b/docs/e2e/screenshots.spec.ts new file mode 100644 index 0000000..3ebe9ab --- /dev/null +++ b/docs/e2e/screenshots.spec.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; + +test.describe('README screenshots', () => { + test.skip( + !process.env.SCREENSHOTS, + 'set SCREENSHOTS=1 to regenerate README screenshots', + ); + + test('capture light and dark homepage screenshots', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/'); + + for (const theme of ['light', 'dark'] as const) { + await page + .locator('html') + .evaluate((el, next) => el.setAttribute('data-theme', next), theme); + await expect(page.locator('.hero')).toBeVisible(); + await page.screenshot({ + path: `../.github/screenshots/${theme}.png`, + }); + } + }); +}); diff --git a/docs/e2e/theme-search-a11y.spec.ts b/docs/e2e/theme-search-a11y.spec.ts index 07f63dc..0a1471b 100644 --- a/docs/e2e/theme-search-a11y.spec.ts +++ b/docs/e2e/theme-search-a11y.spec.ts @@ -74,7 +74,9 @@ test('search returns the seeded token', async ({ page }) => { // The probe token lives in the Components reference page; Pagefind surfaces it // under that page's title. - await expect(page.locator('text=Components').first()).toBeVisible({ + await expect( + page.locator('#starlight__search .pagefind-ui__result-link').first(), + ).toBeVisible({ timeout: 15_000, }); }); @@ -117,9 +119,11 @@ test('home and content pages have no serious/critical a11y violations', async ({ await page.setViewportSize({ width, height }); for (const path of [ '/', + '/guides/', '/guides/authoring-content/', '/reference/components/', '/reference/kitchen-sink/', + '/this-page-does-not-exist/', ]) { await page.goto(path); await expect(page.locator('html')).toHaveAttribute( @@ -132,8 +136,16 @@ test('home and content pages have no serious/critical a11y violations', async ({ el.scrollWidth <= el.clientWidth || el.hasAttribute('tabindex'), ), ); + const pageOverflow = await page.evaluate(() => ({ + scroll: document.documentElement.scrollWidth, + client: document.documentElement.clientWidth, + })); + expect( + pageOverflow.scroll, + `${colorScheme} ${width}px ${path} overflow`, + ).toBe(pageOverflow.client); const results = await new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa']) + .withTags(['wcag2a', 'wcag2aa', 'wcag22aa']) .analyze(); const serious = results.violations.filter( (v) => v.impact === 'serious' || v.impact === 'critical', @@ -149,6 +161,58 @@ test('home and content pages have no serious/critical a11y violations', async ({ } }); +// Nothing in the suite asserted computed type before this: build.test.ts greps class +// names and token mappings, and axe checks contrast. A 13px TOC entry where the design +// binds 14px therefore passed every gate. Size and weight only, never colour — Chromium +// returns colours in their authored space, as the note above explains. +const TYPE_SCALE: Array<{ + selector: string; + fontSize?: string; + fontWeight?: string; +}> = [ + { selector: '#starlight__on-this-page', fontSize: '11px', fontWeight: '500' }, + { selector: '.right-sidebar starlight-toc a', fontSize: '14px' }, + { + selector: '.right-sidebar starlight-toc a[aria-current="true"]', + fontWeight: '500', + }, + { selector: '.pagination-links a', fontSize: '12px', fontWeight: '500' }, + { selector: '.pagination-links .link-title', fontSize: '16px' }, + { selector: '.nbr-page-header h1', fontSize: '34px', fontWeight: '700' }, + { selector: '.nbr-page-meta', fontSize: '13px' }, + { + selector: '.sidebar-content .group-label .large', + fontSize: '14px', + fontWeight: '400', + }, +]; + +test('page chrome renders at the design type scale', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/guides/deployment/build/'); + + // starlight-toc marks the current entry client-side on first intersection, so + // the aria-current row does not exist in the served HTML. + await expect( + page.locator('.right-sidebar starlight-toc a[aria-current="true"]'), + ).toHaveCount(1); + + for (const { selector, fontSize, fontWeight } of TYPE_SCALE) { + const target = page.locator(selector).first(); + await expect(target, `${selector} is missing`).toBeAttached(); + const actual = await target.evaluate((el) => { + const style = getComputedStyle(el); + return { fontSize: style.fontSize, fontWeight: style.fontWeight }; + }); + if (fontSize) { + expect(actual.fontSize, `${selector} font-size`).toBe(fontSize); + } + if (fontWeight) { + expect(actual.fontWeight, `${selector} font-weight`).toBe(fontWeight); + } + } +}); + test('wide viewports centre the content panel when a TOC is present', async ({ page, }) => { @@ -262,3 +326,98 @@ test('exactly one "Site" nav landmark is exposed at each width', async ({ expect(exposed, `${width}px exposed ${exposed} "Site" navs`).toBe(1); } }); + +test('the TOC current entry tracks the last heading at the bottom of the page', async ({ + page, +}) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/reference/components/'); + + const current = page.locator( + '.right-sidebar starlight-toc a[aria-current="true"]', + ); + await expect(current).toHaveCount(1); + + await page.evaluate(() => { + const el = document.scrollingElement ?? document.documentElement; + el.scrollTop = el.scrollHeight; + }); + await expect(current).toHaveCount(1); + await expect(current).toHaveJSProperty('hash', '#guide-cards'); + + await page.locator('#card-grids-at-a-glance').evaluate((el) => { + const nav = + document.querySelector('header')?.getBoundingClientRect().height ?? 0; + window.scrollTo( + 0, + el.getBoundingClientRect().top + window.scrollY - nav - 32, + ); + }); + await expect(current).toHaveCount(1); + await expect(current).not.toHaveJSProperty('hash', '#guide-cards'); + + await page.evaluate(() => { + const el = document.scrollingElement ?? document.documentElement; + el.scrollTop = el.scrollHeight; + }); + await expect(current).toHaveJSProperty('hash', '#guide-cards'); + + await page.locator('#badge-variants').evaluate((el) => { + const nav = + document.querySelector('header')?.getBoundingClientRect().height ?? 0; + window.scrollTo( + 0, + el.getBoundingClientRect().top + window.scrollY - nav - 32, + ); + }); + await expect(current).toHaveCount(1); + + await page.evaluate(() => { + document + .querySelector('.right-sidebar-panel starlight-toc a[href="#full-docs"]') + ?.setAttribute('aria-current', 'true'); + }); + await expect(current).toHaveCount(1); +}); + +test('a page that does not scroll does not force the last TOC entry', async ({ + page, +}) => { + await page.setViewportSize({ width: 1440, height: 3000 }); + await page.goto('/guides/deployment/build/'); + const current = page.locator( + '.right-sidebar starlight-toc a[aria-current="true"]', + ); + await expect(current).toHaveCount(1); + await expect(current).toHaveJSProperty('hash', '#_top'); +}); + +test('the mobile TOC label tracks the last heading at the bottom of the page', async ({ + page, +}) => { + await page.setViewportSize({ width: 375, height: 800 }); + await page.goto('/reference/components/'); + await page.evaluate(() => { + const el = document.scrollingElement ?? document.documentElement; + el.scrollTop = el.scrollHeight; + }); + await expect(page.locator('.display-current')).toHaveText('Guide cards'); +}); + +test('guide chips filter the list in place', async ({ page }) => { + await page.goto('/guides/'); + const visible = page.locator('.nbr-guide-card:visible'); + await expect(visible).toHaveCount(6); + const url = page.url(); + + await page.locator('label[for="gf-getting-started"]').click(); + await expect(visible).toHaveCount(2); + expect(page.url()).toBe(url); + + await page.locator('label[for="gf-deployment"]').click(); + await expect(visible).toHaveCount(2); + + await page.locator('label[for="gf-all"]').click(); + await expect(visible).toHaveCount(6); + expect(page.url()).toBe(url); +}); diff --git a/docs/package.json b/docs/package.json index 49d6c8c..142349a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -8,7 +8,8 @@ "build": "bun run theme && astro build", "build:base": "bun run theme && astro build --base /demo-pack", "preview": "astro preview", - "e2e": "playwright test" + "e2e": "playwright test", + "screenshots": "SCREENSHOTS=1 playwright test e2e/screenshots.spec.ts" }, "dependencies": { "@astrojs/starlight": "^0.41.3", diff --git a/docs/src/content/docs/404.md b/docs/src/content/docs/404.md new file mode 100644 index 0000000..c220862 --- /dev/null +++ b/docs/src/content/docs/404.md @@ -0,0 +1,19 @@ +--- +title: Page not found +description: That page does not exist. +template: splash +tableOfContents: false +hero: + tagline: The page you were looking for does not exist. It may have been moved or the link is out of date. + actions: + - text: Back to home + link: / + variant: primary + - text: Browse the guides + link: /guides/ + variant: secondary +--- + +
+ Found a broken link? Report it on GitHub. +
diff --git a/docs/src/content/docs/getting-started/configuration.mdx b/docs/src/content/docs/getting-started/configuration.mdx index 54d90df..5a98902 100644 --- a/docs/src/content/docs/getting-started/configuration.mdx +++ b/docs/src/content/docs/getting-started/configuration.mdx @@ -36,6 +36,24 @@ nebari({ logoHref: 'https://packs.nebari.dev/' }); See the [Configuration Options reference](/reference/configuration/) for the full option table and defaults. +### `nav` + +Top-level header tabs (Docs / Guides / Reference on this site). When omitted, +the header is byte-identical to stock Starlight. The active tab is the one +that shares the most leading path segments with the current URL (the root +tab is a fallback, not a prefix match). A tab can point at a leaf page and +still light for every page in that section: + +```js +nebari({ + nav: [ + { label: 'Docs', href: '/' }, + { label: 'Guides', href: '/guides/' }, + { label: 'Reference', href: '/reference/configuration/' }, + ], +}); +``` + ## Starlight options worth setting The theme leaves these to you — they're per-pack decisions. diff --git a/docs/src/content/docs/guides/customizing.mdx b/docs/src/content/docs/guides/customizing.mdx index 2b830c9..ca322a5 100644 --- a/docs/src/content/docs/guides/customizing.mdx +++ b/docs/src/content/docs/guides/customizing.mdx @@ -67,9 +67,10 @@ Set light and dark values separately. The theme defines tokens under both ## Add your own components overrides Starlight lets you swap individual layout components. The theme already overrides -`SiteTitle`, `Head`, `Footer`, and `ThemeSelect`. You can override *others* -freely, but if you override one the theme also sets, list yours **after** the -plugin so it wins: +eight: `SiteTitle`, `Head`, `Footer`, `Sidebar`, `ThemeSelect`, `PageTitle`, +`LastUpdated`, and `MarkdownContent`. You can override *others* freely, but if +you override one the theme also sets, list yours **after** the plugin so it +wins: ```js title="astro.config.mjs" starlight({ @@ -82,10 +83,11 @@ starlight({ ``` :::danger -Overriding `SiteTitle`, `Head`, `Footer`, or `ThemeSelect` replaces the theme's -versions of them — you lose the branded logo, self-hosted fonts, footer, or the -Nebari theme toggle respectively. Only do this if you're deliberately replacing -that piece. +Overriding `SiteTitle`, `Head`, `Footer`, `Sidebar`, `ThemeSelect`, `PageTitle`, +`LastUpdated`, or `MarkdownContent` replaces the theme's version — you lose the +branded logo, fonts, splash footer, nav tabs, theme toggle, breadcrumbs, or +table scrolling and TOC bottom-tracking respectively. Only do this if you're deliberately replacing that +piece. ::: ## Where files go diff --git a/docs/src/content/docs/guides/index.mdx b/docs/src/content/docs/guides/index.mdx new file mode 100644 index 0000000..d5ac741 --- /dev/null +++ b/docs/src/content/docs/guides/index.mdx @@ -0,0 +1,50 @@ +--- +title: Guides +description: Task-oriented walkthroughs for installing, authoring, customizing, and deploying a Nebari docs pack. +tableOfContents: false +--- + +import GuideCard from '@nebari/starlight/components/GuideCard.astro'; +import GuideFilter from '@nebari/starlight/components/GuideFilter.astro'; + +How to get a pack's docs on-brand and shipped. Start with install, then pick the +task that matches what you're doing. + + + + + + + + + diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 32eacaa..ee13c7e 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -7,72 +7,70 @@ hero: actions: - text: Get started link: /getting-started/installation/ - icon: right-arrow variant: primary - text: Read the guides - link: /guides/authoring-content/ - icon: open-book - variant: minimal - - text: View on GitHub - link: https://github.com/nebari-dev/starlight - icon: external - variant: minimal + link: /guides/ + variant: secondary --- -import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; +import { Card, CardGrid } from '@astrojs/starlight/components'; -Every Nebari pack ships documentation, and every set of docs should look like it -belongs to the same platform. `@nebari/starlight` is the shared theme that makes -that automatic: add the plugin, write your content, and get a polished, -accessible, on-brand site out of the box. New here? Jump to the -[installation guide](/getting-started/installation/). +## Explore the documentation -## Why teams use it +Everything the theme styles, from a one-line install through to the kitchen sink. - - - Register the `nebari()` plugin in `astro.config.mjs` and the entire theme — - colors, fonts, logo, footer, and social links — is wired up for you. - - - OKLCH design tokens from nebari-design map onto Starlight's variables: a - magenta accent with slate-tinted surfaces, tuned for light and dark. - - - Geist for body and headings, IBM Plex Mono for code — all self-hosted, no - CDN calls, no layout shift. - - - Starlight's built-in Pagefind search is enabled and styled to match, ready - for multisite merging across packs. - - +
+ + + Register the `nebari()` plugin in `astro.config.mjs` and the entire theme — + colors, fonts, logo, footer, and social links — is wired up for you. -## Start here + [Install the theme](/getting-started/installation/) + + + OKLCH design tokens from nebari-design map onto Starlight's variables: a + magenta accent with slate-tinted surfaces, tuned for light and dark. - - - - - - + [Customize tokens](/guides/customizing/) + + + Geist for body and headings, IBM Plex Mono for code — all self-hosted, no + CDN calls, no layout shift. -This site is also a working template. It runs on `@nebari/starlight` itself, so -everything you see — the hero above, the cards, the callouts, the sidebar -groups — is the theme in action. Fork it as the starting point for your own pack. + [See it in content](/guides/authoring-content/) + + + Starlight's built-in Pagefind search is enabled and styled to match, ready + for multisite merging across packs. + + [Open the components](/reference/components/) + + + Plugin options, the Starlight settings the theme manages, and what a pack + still owns. + + [Read the reference](/reference/configuration/) + + + Every content surface on one page — callouts, tabs, tables, steps, figures + — so you can check the theme against the design. + + [Browse the sink](/reference/kitchen-sink/) + + +
+ +## Popular pages + +
+ +- [Installation](/getting-started/installation/) +- [Quickstart](/getting-started/quickstart/) +- [Authoring content](/guides/authoring-content/) +- [Customizing the theme](/guides/customizing/) +- [Configuration](/reference/configuration/) +- [Kitchen sink](/reference/kitchen-sink/) +- [GitHub](https://github.com/nebari-dev) +- [nebari.dev](https://nebari.dev) + +
diff --git a/docs/src/content/docs/reference/components.mdx b/docs/src/content/docs/reference/components.mdx index f3f6a0e..3546acc 100644 --- a/docs/src/content/docs/reference/components.mdx +++ b/docs/src/content/docs/reference/components.mdx @@ -5,9 +5,9 @@ description: The Starlight components available in Nebari docs packs, with impor import { Badge, Card, CardGrid } from '@astrojs/starlight/components'; -Nebari Starlight styles the standard Starlight component set — it doesn't add new -components or dependencies. Everything below is imported from -`@astrojs/starlight/components` and works in any `.mdx` page. +Nebari Starlight styles the standard Starlight component set and adds one +layout helper, `GuideCard`, for the guides landing page. Everything else is +imported from `@astrojs/starlight/components` and works in any `.mdx` page. ```js import { Card, CardGrid, Tabs, TabItem, Steps, FileTree, Badge, Aside } @@ -27,6 +27,8 @@ import { Card, CardGrid, Tabs, TabItem, Steps, FileTree, Badge, Aside } | `Aside` | Callouts (also via `:::note` syntax) | | | `LinkButton` | Prominent call-to-action link | | | `Icon` | Built-in icon set | | +| `GuideCard` | Guides-landing row (badge, read time, arrow) | | +| `GuideFilter` | Category chips that filter a stack of cards | | ## Badge variants @@ -77,3 +79,33 @@ ships search enabled. For prop-level detail on any component, see the upstream [Starlight component reference](https://starlight.astro.build/components/using-components/). This theme changes their appearance, not their API. + +## Guide cards + +`GuideCard` is the one extra component. Import it from the theme, not from +Starlight. Wrap a stack in `GuideFilter` to get the category chips; it also +supplies `.nbr-guide-list`, so the page drops breadcrumbs, the meta row, and +the title hairline. + +```js +import GuideCard from '@nebari/starlight/components/GuideCard.astro'; +import GuideFilter from '@nebari/starlight/components/GuideFilter.astro'; +``` + +```mdx + + + +``` + +`categories` must match each card's `category` prop. The read time is derived +from the linked page, so it stays in step with the article's own meta row; pass +`time` to override it. See the [guides +landing](/guides/) for a working example, and the [layout +classes](/reference/configuration/#layout-classes) for the other landing +contracts (`.nbr-grid-3`, `.nbr-cols-2`, `.nbr-chip`). diff --git a/docs/src/content/docs/reference/configuration.mdx b/docs/src/content/docs/reference/configuration.mdx index 4fabdc9..0ff32b7 100644 --- a/docs/src/content/docs/reference/configuration.mdx +++ b/docs/src/content/docs/reference/configuration.mdx @@ -14,14 +14,20 @@ sets on your behalf. For task-oriented guidance, see Options passed to `nebari(options)`: -| Option | Type | Default | Description | -| ----------- | ------------------ | -------------------------- | --------------------------------------------------------------------------- | -| `logoHref` | `string` | site base (`BASE_URL`) | URL the header logo links to. Set to a portal root for multi-pack domains. | +| Option | Type | Default | Description | +| ---------- | ---------------------------------------- | ---------------------- | --------------------------------------------------------------------------- | +| `logoHref` | `string` | site base (`BASE_URL`) | URL the header logo links to. Set to a portal root for multi-pack domains. | +| `nav` | `{ label: string; href: string }[]` | unset | Top-level header tabs. Absent, the header is byte-identical to Starlight. | ```ts interface NebariThemeOptions { /** URL the header logo links to. Defaults to the site's own base. */ logoHref?: string; + /** + * Top-level header tabs. When omitted, the header is byte-identical to + * stock Starlight. + */ + nav?: Array<{ label: string; href: string }>; } ``` @@ -30,27 +36,48 @@ interface NebariThemeOptions { When you register `nebari()`, it updates your Starlight config as follows. All of these are merged **before** your own config, so your values take precedence. -| Setting | What the theme adds | Overridable | -| ----------- | ------------------------------------------------------------------------------------------------------- | ----------- | -| `customCss` | Font faces, Nebari tokens, theme mapping, and component styles — prepended before your own `customCss`. | Yes | -| `components` | Overrides `SiteTitle`, `Head`, `Footer`, and `ThemeSelect`. | Yes | -| `social` | Prepends a GitHub link to `github.com/nebari-dev`. | Yes | +| Setting | What the theme adds | Overridable | +| ------------ | ------------------------------------------------------------------------------------------------------- | ----------- | +| `customCss` | Font faces, Nebari tokens, theme mapping, and component styles — prepended before your own `customCss`. | Yes | +| `components` | Overrides the eight components listed below. | Yes | +| `social` | Prepends a GitHub link to `github.com/nebari-dev`. | Yes | +| `lastUpdated`| Defaults to `true`, so a date appears on pages that had none. | Yes | ### Overridden components -| Component | Status | Why the theme overrides it | -| ------------- | --------------------------------- | --------------------------------------------------- | -| `SiteTitle` | | Renders the Nebari logo in the header. | -| `Head` | | Injects self-hosted font preloads. | -| `Footer` | | Adds the branded Nebari footer. | -| `ThemeSelect` | | Swaps the `` for the icon toggle. | +| `PageTitle` | | Renders breadcrumbs and the updated / read-time meta row. | +| `LastUpdated` | | Suppresses the duplicate date Starlight prints in the footer. | +| `MarkdownContent`| | Wraps tables so wide grids and long cells scroll instead of wrap, and tracks the last TOC heading at the bottom of the page. | :::caution -Setting any of the four components above in your own `components` map replaces the -theme's version and drops the corresponding branding. See +Setting any of the eight components above in your own `components` map replaces +the theme's version and drops the corresponding feature. See [Customizing the Theme](/guides/customizing/). ::: +## Layout classes + +These class names are a documented CSS contract. Use them in Markdown so a pack +gets the landing and guides layouts without a versioned component API. + +| Class | Where | What it does | +| ---------------- | --------------------------------------------- | ---------------------------------------------------------------------------- | +| `.nbr-grid-3` | wrapping a `` | Three-up cards that ladder to two, then one. | +| `.nbr-cols-2` | wrapping a Markdown list | Two-column popular-pages list, column-major, with trailing arrows. | +| `.nbr-chip` | a `