diff --git a/.changeset/plenty-donkeys-lay.md b/.changeset/plenty-donkeys-lay.md new file mode 100644 index 0000000000..cb041fbdeb --- /dev/null +++ b/.changeset/plenty-donkeys-lay.md @@ -0,0 +1,21 @@ +--- +'@astrojs/starlight': minor +--- + +Add support for overriding Starlight’s built-in components + +⚠️ **BREAKING CHANGE** — The page footer is now included on pages with `template: splash` in their frontmatter. Previously, this was not the case. If you are using `template: splash` and want to continue to hide footer elements, disable them in your frontmatter: + +```md +--- +title: Landing page +template: splash +# Disable unwanted footer elements as needed +editUrl: false +lastUpdated: false +prev: false +next: false +--- +``` + +⚠️ **BREAKING CHANGE** — This change involved refactoring the structure of some of Starlight’s built-in components slightly. If you were previously overriding these using other techniques, you may need to adjust your code. \ No newline at end of file diff --git a/docs/src/content/docs/guides/overriding-components.md b/docs/src/content/docs/guides/overriding-components.md new file mode 100644 index 0000000000..71ea7e1dc4 --- /dev/null +++ b/docs/src/content/docs/guides/overriding-components.md @@ -0,0 +1,133 @@ +--- +title: Overriding Components +description: Learn how to override Starlight’s built-in components to add custom elements to your documentation site’s UI. +sidebar: + badge: New +--- + +Starlight’s default UI and configuration options are designed to be flexible and work for a range of content. Much of Starlight's default appearance can be customized with [CSS](/guides/css-and-tailwind/) and [configuration options](/guides/customization/). + +When you need more than what’s possible out of the box, Starlight supports building your own custom components to extend or override (completely replace) its default components. + +## When to override + +Overriding Starlight’s default components can be useful when: + +- You want to change how a part of Starlight’s UI looks in a way not possible with [custom CSS](/css-and-tailwind/). +- You want to change how a part of Starlight’s UI behaves. +- You want to add some additional UI alongside Starlight’s existing UI. + +## How to override + +1. Choose the Starlight component you want to override. + You can find a full list of components in the [Overrides Reference](/reference/overrides/). + + This example will override Starlight’s [`SocialIcons`](/reference/overrides/#socialicons) component in the page nav bar. + +2. Create an Astro component to replace the Starlight component with. + This example renders a contact link. + + ```astro + --- + // src/components/EmailLink.astro + import type { Props } from '@astrojs/starlight/props'; + --- + + E-mail Me + ``` + +3. Tell Starlight to use your custom component in the [`components`](/reference/configuration/#components) configuration option in `astro.config.mjs`: + + ```js {9-12} + // astro.config.mjs + import { defineConfig } from 'astro/config'; + import starlight from '@astrojs/starlight'; + + export default defineConfig({ + integrations: [ + starlight({ + title: 'My Docs with Overrides', + components: { + // Override the default `SocialLinks` component. + SocialIcons: './src/components/EmailLink.astro', + }, + }), + ], + }); + ``` + +## Reuse a built-in component + +You can build with Starlight’s default UI components just as you would with your own: importing and rendering them in your own custom components. This allows you to keep all of Starlight's basic UI within your design, while adding extra UI alongside them. + +The example below shows a custom component that renders an e-mail link along with the default `SocialLinks` component: + +```astro {4,8} +--- +// src/components/EmailLink.astro +import type { Props } from '@astrojs/starlight/props'; +import Default from '@astrojs/starlight/SocialIcons.astro'; +--- + +E-mail Me + +``` + +When rendering a built-in component inside a custom component: + +- Spread `Astro.props` into it. This makes sure that it receives all the data it needs to render. +- Add a [``](https://docs.astro.build/en/core-concepts/astro-components/#slots) inside the default component. This makes sure that if the component is passed any child elements, Astro knows where to render them. + +## Use page data + +When overriding a Starlight component, your custom implementation receives a standard `Astro.props` object containing all the data for the current page. +This allows you to use these values to control how your component template renders. + +For example, you can read the page’s frontmatter values as `Astro.props.entry.data`. In the following example, a replacement [`PageTitle`](/reference/overrides/#pagetitle) component uses this to display the current page’s title: + +```astro {5} "{title}" +--- +// src/components/Title.astro +import type { Props } from '@astrojs/starlight/props'; + +const { title } = Astro.props.entry.data; +--- + +

{title}

+ + +``` + +Learn more about all the available props in the [Overrides Reference](/reference/overrides/#prop-types). + +### Only override on specific pages + +Component overrides apply to all pages. However, you can conditionally render using values from `Astro.props` to determine when to show your custom UI, when to show Starlight’s default UI, or even when to show something entirely different. + +In the following example, a component overriding Starlight's [`Footer`](/reference/overrides/#footer-1) displays "Built with Starlight 🌟" on the homepage only, and otherwise shows the default footer on all other pages: + +```astro +--- +// src/components/ConditionalFooter.astro +import type { Props } from '@astrojs/starlight/props'; +import Default from '@astrojs/starlight/Footer.astro'; + +const isHomepage = Astro.props.slug === ''; +--- + +{ + isHomepage ? ( + + ) : ( + + + + ) +} +``` + +Learn more about conditional rendering in [Astro’s Template Syntax guide](https://docs.astro.build/en/core-concepts/astro-syntax/#dynamic-html). diff --git a/docs/src/content/docs/guides/sidebar.mdx b/docs/src/content/docs/guides/sidebar.mdx index 6a732e6dca..332563667d 100644 --- a/docs/src/content/docs/guides/sidebar.mdx +++ b/docs/src/content/docs/guides/sidebar.mdx @@ -1,8 +1,6 @@ --- title: Sidebar Navigation description: Learn how to set up and customize your Starlight site’s sidebar navigation links. -sidebar: - badge: New --- import FileTree from '../../../components/file-tree.astro'; diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 6836d86546..6cef6afac5 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -5,6 +5,8 @@ head: content: Starlight 🌟 Build documentation sites with Astro description: Starlight helps you build beautiful, high-performance documentation websites with Astro. template: splash +editUrl: false +lastUpdated: false banner: content: | diff --git a/docs/src/content/docs/reference/configuration.md b/docs/src/content/docs/reference/configuration.md index ad2d859109..b392f7f514 100644 --- a/docs/src/content/docs/reference/configuration.md +++ b/docs/src/content/docs/reference/configuration.md @@ -436,3 +436,19 @@ Sets the delimiter between page title and site title in the page’s `` t By default, every page has a `<title>` of `Page Title | Site Title`. For example, this page is titled “Configuration Reference” and this site is titled “Starlight”, so the `<title>` for this page is “Configuration Reference | Starlight”. + +### `components` + +**type:** `Record<string, string>` + +Provide the paths to components to override Starlight’s default implementations. + +```js +starlight({ + components: { + SocialLinks: './src/components/MySocialLinks.astro', + }, +}); +``` + +See the [Overrides Reference](/reference/overrides/) for details of all the components that you can override. diff --git a/docs/src/content/docs/reference/overrides.md b/docs/src/content/docs/reference/overrides.md new file mode 100644 index 0000000000..6b2ea2c42b --- /dev/null +++ b/docs/src/content/docs/reference/overrides.md @@ -0,0 +1,374 @@ +--- +title: Overrides Reference +description: An overview of the components and component props supported by Starlight overrides. +tableOfContents: + maxHeadingLevel: 4 +--- + +You can override Starlight’s built-in components by providing paths to replacement components in Starlight’s [`components`](/reference/configuration#components) configuration option. +This page lists all components available to override and links to their default implementations on GitHub. + +Learn more in the [Guide to Overriding Components](/guides/overriding-components/). + +## Component props + +All components can access a standard `Astro.props` object that contains information about the current page. + +To type your custom components, import the `Props` type from Starlight: + +```astro +--- +import type { Props } from '@astrojs/starlight/props'; + +const { hasSidebar } = Astro.props; +// ^ type: boolean +--- +``` + +This will give you autocomplete and types when accessing `Astro.props`. + +### Props + +Starlight will pass the following props to your custom components. + +#### `dir` + +**Type:** `'ltr' | 'rtl'` + +Page writing direction. + +#### `lang` + +**Type:** `string` + +BCP-47 language tag for this page’s locale, e.g. `en`, `zh`, or `pt-BR`. + +#### `locale` + +**Type:** `string | undefined` + +The base path at which a language is served. `undefined` for root locale slugs. + +#### `slug` + +**Type:** `string` + +The slug for this page generated from the content filename. + +#### `id` + +**Type:** `string` + +The unique ID for this page based on the content filename. + +#### `isFallback` + +**Type:** `true | undefined` + +`true` if this page is untranslated in the current language and using fallback content from the default locale. +Only used in multilingual sites. + +#### `entryMeta` + +**Type:** `{ dir: 'ltr' | 'rtl'; lang: string }` + +Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. + +#### `entry` + +The Astro content collection entry for the current page. +Includes frontmatter values for the current page at `entry.data`. + +```ts +entry: { + data: { + title: string; + description: string | undefined; + // etc. + } +} +``` + +Learn more about the shape of this object in [Astro’s Collection Entry Type](https://docs.astro.build/en/reference/api-reference/#collection-entry-type) reference. + +#### `sidebar` + +**Type:** `SidebarEntry[]` + +Site navigation sidebar entries for this page. + +#### `hasSidebar` + +**Type:** `boolean` + +Whether or not the sidebar should be displayed on this page. + +#### `pagination` + +**Type:** `{ prev?: Link; next?: Link }` + +Links to the previous and next page in the sidebar if enabled. + +#### `toc` + +**Type:** `{ minHeadingLevel: number; maxHeadingLevel: number; items: TocItem[] } | undefined` + +Table of contents for this page if enabled. + +#### `headings` + +**Type:** `{ depth: number; slug: string; text: string }[]` + +Array of all Markdown headings extracted from the current page. +Use [`toc`](#toc) instead if you want to build a table of contents that respects Starlight’s configuration options. + +#### `lastUpdated` + +**Type:** `Date | undefined` + +JavaScript `Date` object representing when this page was last updated if enabled. + +#### `editUrl` + +**Type:** `URL | undefined` + +`URL` object for the address where this page can be edited if enabled. + +--- + +## Components + +### Head + +These components are rendered inside each page’s `<head>` element. +They should only include [elements permitted inside `<head>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head#see_also). + +#### `Head` + +**Default component:** [`Head.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Head.astro) + +Component rendered inside each page’s `<head>`. +Includes important tags including `<title>`, and `<meta charset="utf-8">`. + +Override this component as a last resort. +Prefer the [`head`](/reference/configuration#head) option Starlight config if possible. + +#### `ThemeProvider` + +**Default component:** [`ThemeProvider.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeProvider.astro) + +Component rendered inside `<head>` that sets up dark/light theme support. +The default implementation includes an inline script and a `<template>` used by the script in [`<ThemeSelect />`](#themeselect). + +--- + +### Accessibility + +#### `SkipLink` + +**Default component:** [`SkipLink.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/SkipLink.astro) + +Component rendered as the first element inside `<body>` which links to the main page content for accessibility. +The default implementation is hidden until a user focuses it by tabbing with their keyboard. + +--- + +### Layout + +These components are responsible for laying out Starlight’s components and managing views across different breakpoints. +Overriding these comes with significant complexity. +When possible, prefer overriding a lower-level component. + +#### `PageFrame` + +**Default component:** [`PageFrame.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageFrame.astro) + +Layout component wrapped around most of the page content. +The default implementation sets up the header–sidebar–main layout and includes `header` and `sidebar` named slots along with a default slot for the main content. +It also renders [`<MobileMenuToggle />`](#mobilemenutoggle) to support toggling the sidebar navigation on small (mobile) viewports. + +#### `MobileMenuToggle` + +**Default component:** [`MobileMenuToggle.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuToggle.astro) + +Component rendered inside [`<PageFrame>`](#pageframe) that is responsible for toggling the sidebar navigation on small (mobile) viewports. + +#### `TwoColumnContent` + +**Default component:** [`TwoColumnContent.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/TwoColumnContent.astro) + +Layout component wrapped around the main content column and right sidebar (table of contents). +The default implementation handles the switch between a single-column, small-viewport layout and a two-column, larger-viewport layout. + +--- + +### Header + +These components render Starlight’s top navigation bar. + +#### `Header` + +**Default component:** [`Header.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Header.astro) + +Header component displayed at the top of every page. +The default implementation displays [`<SiteTitle />`](#sitetitle), [`<Search />`](#search), [`<SocialIcons />`](#socialicons), [`<ThemeSelect />`](#themeselect), and [`<LanguageSelect />`](#languageselect). + +#### `SiteTitle` + +**Default component:** [`SiteTitle.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/SiteTitle.astro) + +Component rendered at the start of the site header to render the site title. +The default implementation includes logic for rendering logos defined in Starlight config. + +#### `Search` + +**Default component:** [`Search.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro) + +Component used to render Starlight’s search UI. +The default implementation includes the button in the header and the code for displaying a search modal when it is clicked and loading [Pagefind’s UI](https://pagefind.app/). + +#### `SocialIcons` + +**Default component:** [`SocialIcons.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/SocialIcons.astro) + +Component rendered in the site header including social icon links. +The default implementation uses the [`social`](/reference/configuration#social) option in Starlight config to render icons and links. + +#### `ThemeSelect` + +**Default component:** [`ThemeSelect.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeSelect.astro) + +Component rendered in the site header that allows users to select their preferred color scheme. + +#### `LanguageSelect` + +**Default component:** [`LanguageSelect.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/LanguageSelect.astro) + +Component rendered in the site header that allows users to switch to a different language. + +--- + +### Global Sidebar + +Starlight’s global sidebar includes the main site navigation. +On narrow viewports this is hidden behind a drop-down menu. + +#### `Sidebar` + +**Default component:** [`Sidebar.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Sidebar.astro) + +Component rendered before page content that contains global navigation. +The default implementation displays as a sidebar on wide enough viewports and inside a drop-down menu on small (mobile) viewports. +It also renders [`<MobileMenuFooter />`](#mobilemenufooter) to show additional items inside the mobile menu. + +#### `MobileMenuFooter` + +**Default component:** [`MobileMenuFooter.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuFooter.astro) + +Component rendered at the bottom of the mobile drop-down menu. +The default implementation renders [`<ThemeSelect />`](#themeselect) and [`<LanguageSelect />`](#languageselect). + +--- + +### Page Sidebar + +Starlight’s page sidebar is responsible for displaying a table of contents outlining the current page’s subheadings. +On narrow viewports this collapse into a sticky, drop-down menu. + +#### `PageSidebar` + +**Default component:** [`PageSidebar.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageSidebar.astro) + +Component rendered before the main page’s content to display a table of contents. +The default implementation renders [`<TableOfContents />`](#tableofcontents) and [`<MobileTableOfContents />`](#mobiletableofcontents). + +#### `TableOfContents` + +**Default component:** [`TableOfContents.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/TableOfContents.astro) + +Component that renders the current page’s table of contents on wider viewports. + +#### `MobileTableOfContents` + +**Default component:** [`MobileTableOfContents.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileTableOfContents.astro) + +Component that renders the current page’s table of contents on small (mobile) viewports. + +--- + +### Content + +These components are rendered in the main column of page content. + +#### `Banner` + +**Default component:** [`Banner.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Banner.astro) + +Banner component rendered at the top of each page. +The default implementation uses the page’s [`banner`](/reference/frontmatter#banner) frontmatter value to decide whether or not to render. + +#### `ContentPanel` + +**Default component:** [`ContentPanel.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/ContentPanel.astro) + +Layout component used to wrap sections of the main content column. + +#### `PageTitle` + +**Default component:** [`PageTitle.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageTitle.astro) + +Component containing the `<h1>` element for the current page. + +Implementations should ensure they set `id="_top"` on the `<h1>` element as in the default implementation. + +#### `FallbackContentNotice` + +**Default component:** [`FallbackContentNotice.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/FallbackContentNotice.astro) + +Notice displayed to users on pages where a translation for the current language is not available. +Only used on multilingual sites. + +#### `Hero` + +**Default component:** [`Hero.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Hero.astro) + +Component rendered at the top of the page when [`hero`](/reference/frontmatter#hero) is set in frontmatter. +The default implementation shows a large title, tagline, and call-to-action links alongside an optional image. + +#### `MarkdownContent` + +**Default component:** [`MarkdownContent.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/MarkdownContent.astro) + +Component rendered around each page’s main content. +The default implementation sets up basic styles to apply to Markdown content. + +--- + +### Footer + +These components are rendered at the bottom of the main column of page content. + +#### `Footer` + +**Default component:** [`Footer.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Footer.astro) + +Footer component displayed at the bottom of each page. +The default implementation displays [`<LastUpdated />`](#lastupdated), [`<Pagination />`](#pagination), and [`<EditLink />`](#editlink). + +#### `LastUpdated` + +**Default component:** [`LastUpdated.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/LastUpdated.astro) + +Component rendered in the page footer to display the last-updated date. + +#### `EditLink` + +**Default component:** [`EditLink.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/EditLink.astro) + +Component rendered in the page footer to display a link to where the page can be edited. + +#### `Pagination` + +**Default component:** [`Pagination.astro`](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Pagination.astro) + +Component rendered in the page footer to display navigation arrows between previous/next pages. diff --git a/packages/starlight/404.astro b/packages/starlight/404.astro index 3b10bf4574..913c5143f6 100644 --- a/packages/starlight/404.astro +++ b/packages/starlight/404.astro @@ -2,7 +2,8 @@ import { getEntry } from 'astro:content'; import config from 'virtual:starlight/user-config'; import EmptyContent from './components/EmptyMarkdown.md'; -import Page from './layout/Page.astro'; +import Page from './components/Page.astro'; +import { generateRouteData } from './utils/route-data'; import type { StarlightDocsEntry } from './utils/routing'; import { useTranslations } from './utils/translations'; @@ -24,6 +25,8 @@ const fallbackEntry: StarlightDocsEntry = { editUrl: false, head: [], hero: { tagline: t('404.text'), actions: [] }, + pagefind: false, + sidebar: { hidden: false }, }, render: async () => ({ Content: EmptyContent, @@ -35,8 +38,10 @@ const fallbackEntry: StarlightDocsEntry = { const userEntry = await getEntry('docs', '404'); const entry = userEntry || fallbackEntry; const { Content, headings } = await entry.render(); +const route = generateRouteData({ + props: { ...entryMeta, entryMeta, headings, entry, id: entry.id, slug: entry.slug }, + url: Astro.url, +}); --- -<Page {headings} entry={entry} id={entry.id} slug={entry.slug} {...entryMeta} {entryMeta}> - <Content /> -</Page> +<Page {...route}><Content /></Page> diff --git a/packages/starlight/__tests__/basics/route-data.test.ts b/packages/starlight/__tests__/basics/route-data.test.ts new file mode 100644 index 0000000000..13522d0224 --- /dev/null +++ b/packages/starlight/__tests__/basics/route-data.test.ts @@ -0,0 +1,87 @@ +import { expect, test, vi } from 'vitest'; +import { generateRouteData } from '../../utils/route-data'; +import { routes } from '../../utils/routing'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['getting-started.mdx', { title: 'Splash', template: 'splash' }], + ['showcase.mdx', { title: 'ToC Disabled', tableOfContents: false }], + ['environmental-impact.md', { title: 'Explicit update date', lastUpdated: new Date() }], + ], + }) +); + +test('adds data to route shape', () => { + const route = routes[0]!; + const data = generateRouteData({ + props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] }, + url: new URL('https://example.com'), + }); + expect(data.hasSidebar).toBe(true); + expect(data).toHaveProperty('lastUpdated'); + expect(data.toc).toMatchInlineSnapshot(` + { + "items": [ + { + "children": [], + "depth": 2, + "slug": "_top", + "text": "Overview", + }, + ], + "maxHeadingLevel": 3, + "minHeadingLevel": 2, + } + `); + expect(data.pagination).toMatchInlineSnapshot(` + { + "next": { + "attrs": {}, + "badge": undefined, + "href": "/environmental-impact/", + "isCurrent": false, + "label": "Explicit update date", + "type": "link", + }, + "prev": undefined, + } + `); + expect(data.sidebar.map((entry) => entry.label)).toMatchInlineSnapshot(` + [ + "Home Page", + "Explicit update date", + "Splash", + "ToC Disabled", + ] + `); +}); + +test('disables table of contents for splash template', () => { + const route = routes[1]!; + const data = generateRouteData({ + props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] }, + url: new URL('https://example.com/getting-started/'), + }); + expect(data.toc).toBeUndefined(); +}); + +test('disables table of contents if frontmatter includes `tableOfContents: false`', () => { + const route = routes[2]!; + const data = generateRouteData({ + props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] }, + url: new URL('https://example.com/showcase/'), + }); + expect(data.toc).toBeUndefined(); +}); + +test('uses explicit last updated date from frontmatter', () => { + const route = routes[3]!; + const data = generateRouteData({ + props: { ...route, headings: [{ depth: 1, slug: 'heading-1', text: 'Heading 1' }] }, + url: new URL('https://example.com/showcase/'), + }); + expect(data.lastUpdated).toBeInstanceOf(Date); + expect(data.lastUpdated).toEqual(route.entry.data.lastUpdated); +}); diff --git a/packages/starlight/__tests__/basics/toc.test.ts b/packages/starlight/__tests__/basics/toc.test.ts index 51745d7b81..ae1136f969 100644 --- a/packages/starlight/__tests__/basics/toc.test.ts +++ b/packages/starlight/__tests__/basics/toc.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { generateToC } from '../../components/TableOfContents/generateToC'; +import { generateToC } from '../../utils/generateToC'; const defaultOpts = { minHeadingLevel: 2, maxHeadingLevel: 3, title: 'Overview' }; diff --git a/packages/starlight/__tests__/edit-url/edit-url.test.ts b/packages/starlight/__tests__/edit-url/edit-url.test.ts new file mode 100644 index 0000000000..25ab98aaeb --- /dev/null +++ b/packages/starlight/__tests__/edit-url/edit-url.test.ts @@ -0,0 +1,48 @@ +import { expect, test, vi } from 'vitest'; +import { generateRouteData } from '../../utils/route-data'; +import { routes } from '../../utils/routing'; + +vi.mock('astro:content', async () => + (await import('../test-utils')).mockedAstroContent({ + docs: [ + ['index.mdx', { title: 'Home Page' }], + ['getting-started.mdx', { title: 'Getting Started' }], + [ + 'showcase.mdx', + { title: 'Custom edit link', editUrl: 'https://example.com/custom-edit?link' }, + ], + ], + }) +); + +test('synthesizes edit URL using file location and `editLink.baseUrl`', () => { + { + const route = routes[0]!; + const data = generateRouteData({ + props: { ...route, headings: [] }, + url: new URL('https://example.com'), + }); + expect(data.editUrl?.href).toBe( + 'https://github.com/withastro/starlight/edit/main/docs/src/content/docs/index.mdx' + ); + } + { + const route = routes[1]!; + const data = generateRouteData({ + props: { ...route, headings: [] }, + url: new URL('https://example.com'), + }); + expect(data.editUrl?.href).toBe( + 'https://github.com/withastro/starlight/edit/main/docs/src/content/docs/getting-started.mdx' + ); + } +}); + +test('uses frontmatter `editUrl` if defined', () => { + const route = routes[2]!; + const data = generateRouteData({ + props: { ...route, headings: [] }, + url: new URL('https://example.com'), + }); + expect(data.editUrl?.href).toBe('https://example.com/custom-edit?link'); +}); diff --git a/packages/starlight/__tests__/edit-url/vitest.config.ts b/packages/starlight/__tests__/edit-url/vitest.config.ts new file mode 100644 index 0000000000..4035c23406 --- /dev/null +++ b/packages/starlight/__tests__/edit-url/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineVitestConfig } from '../test-config'; + +export default defineVitestConfig({ + title: 'Docs With Edit Links', + editLink: { + baseUrl: 'https://github.com/withastro/starlight/edit/main/docs/', + }, +}); diff --git a/packages/starlight/__tests__/test-config.ts b/packages/starlight/__tests__/test-config.ts index 9d06a69312..8d0c42e64a 100644 --- a/packages/starlight/__tests__/test-config.ts +++ b/packages/starlight/__tests__/test-config.ts @@ -6,11 +6,9 @@ import { vitePluginStarlightUserConfig } from '../integrations/virtual-user-conf import { StarlightConfigSchema } from '../utils/user-config'; export function defineVitestConfig(config: z.input<typeof StarlightConfigSchema>) { + const root = new URL('./', import.meta.url); + const srcDir = new URL('./src/', root); return getViteConfig({ - plugins: [ - vitePluginStarlightUserConfig(StarlightConfigSchema.parse(config), { - root: new URL(import.meta.url), - }), - ], + plugins: [vitePluginStarlightUserConfig(StarlightConfigSchema.parse(config), { root, srcDir })], }); } diff --git a/packages/starlight/components/Banner.astro b/packages/starlight/components/Banner.astro index e6307bec18..107baa707d 100644 --- a/packages/starlight/components/Banner.astro +++ b/packages/starlight/components/Banner.astro @@ -1,10 +1,10 @@ --- -interface Props { - content: string; -} +import type { Props } from '../props'; + +const { banner } = Astro.props.entry.data; --- -<div class="sl-banner" set:html={Astro.props.content} /> +{banner && <div class="sl-banner" set:html={banner.content} />} <style> .sl-banner { diff --git a/packages/starlight/components/ContentPanel.astro b/packages/starlight/components/ContentPanel.astro index d4089408f5..3f23fcd053 100644 --- a/packages/starlight/components/ContentPanel.astro +++ b/packages/starlight/components/ContentPanel.astro @@ -1,3 +1,7 @@ +--- +import type { Props } from '../props'; +--- + <div class="content-panel"> <div class="sl-container"><slot /></div> </div> diff --git a/packages/starlight/components/EditLink.astro b/packages/starlight/components/EditLink.astro index 4de013482f..f5f5f65b1c 100644 --- a/packages/starlight/components/EditLink.astro +++ b/packages/starlight/components/EditLink.astro @@ -1,34 +1,15 @@ --- -import type { CollectionEntry } from 'astro:content'; -import config from 'virtual:starlight/user-config'; -import project from 'virtual:starlight/project-context'; -import { useTranslations } from '../utils/translations'; import Icon from '../user-components/Icon.astro'; - -interface Props { - data: CollectionEntry<'docs'>['data']; - id: CollectionEntry<'docs'>['id']; - locale: string | undefined; -} +import type { Props } from '../props'; +import { useTranslations } from '../utils/translations'; const t = useTranslations(Astro.props.locale); -const { editUrl } = Astro.props.data; -const srcPath = project.srcDir.replace(project.root, ''); - -let { baseUrl } = config.editLink; -if (baseUrl && baseUrl.at(-1) !== '/') baseUrl += '/'; - -const url = - typeof editUrl === 'string' - ? editUrl - : baseUrl - ? baseUrl + srcPath + 'content/docs/' + Astro.props.id - : undefined; +const { editUrl } = Astro.props; --- { - editUrl !== false && url && ( - <a href={url} class="sl-flex"> + editUrl && ( + <a href={editUrl} class="sl-flex"> <Icon name="pencil" size="1.2em" /> {t('page.editLink')} </a> diff --git a/packages/starlight/components/FallbackContentNotice.astro b/packages/starlight/components/FallbackContentNotice.astro index 9b7ca623d5..171eeb157a 100644 --- a/packages/starlight/components/FallbackContentNotice.astro +++ b/packages/starlight/components/FallbackContentNotice.astro @@ -1,10 +1,7 @@ --- -import { useTranslations } from '../utils/translations'; import Icon from '../user-components/Icon.astro'; - -interface Props { - locale: string | undefined; -} +import type { Props } from '../props'; +import { useTranslations } from '../utils/translations'; const t = useTranslations(Astro.props.locale); --- diff --git a/packages/starlight/components/Footer.astro b/packages/starlight/components/Footer.astro index 2d14dee73e..f5936f01e4 100644 --- a/packages/starlight/components/Footer.astro +++ b/packages/starlight/components/Footer.astro @@ -1,42 +1,15 @@ --- -import config from 'virtual:starlight/user-config'; -import { type SidebarEntry, getPrevNextLinks } from '../utils/navigation'; -import type { StarlightDocsEntry } from '../utils/routing'; -import type { LocaleData } from '../utils/slugs'; +import type { Props } from '../props'; -import LastUpdated from '../components/LastUpdated.astro'; -import PrevNextLinks from '../components/PrevNextLinks.astro'; -import EditLink from './EditLink.astro'; - -interface Props extends LocaleData { - entry: StarlightDocsEntry; - sidebar: SidebarEntry[]; -} - -const { entry, dir, lang, locale, sidebar } = Astro.props; -const prevNextLinks = getPrevNextLinks(sidebar, config.pagination, { - prev: entry.data.prev, - next: entry.data.next, -}); +import { EditLink, LastUpdated, Pagination } from 'virtual:starlight/components'; --- <footer> <div class="meta sl-flex"> - {config.editLink.baseUrl && <EditLink data={entry.data} id={entry.id} {locale} />} - { - (entry.data.lastUpdated ?? config.lastUpdated) && ( - <LastUpdated - id={entry.id} - {lang} - lastUpdated={ - typeof entry.data.lastUpdated !== 'boolean' ? entry.data.lastUpdated : undefined - } - {locale} - /> - ) - } + <EditLink {...Astro.props} /> + <LastUpdated {...Astro.props} /> </div> - <PrevNextLinks {...prevNextLinks} {dir} {locale} /> + <Pagination {...Astro.props} /> </footer> <style> diff --git a/packages/starlight/components/HeadSEO.astro b/packages/starlight/components/Head.astro similarity index 92% rename from packages/starlight/components/HeadSEO.astro rename to packages/starlight/components/Head.astro index e744103cc5..ec2431fc1e 100644 --- a/packages/starlight/components/HeadSEO.astro +++ b/packages/starlight/components/Head.astro @@ -1,18 +1,15 @@ --- -import type { CollectionEntry, z } from 'astro:content'; +import type { z } from 'astro/zod'; import config from 'virtual:starlight/user-config'; +import { version } from '../package.json'; import type { HeadConfigSchema } from '../schemas/head'; +import { fileWithBase } from '../utils/base'; import { createHead } from '../utils/head'; import { localizedUrl } from '../utils/localizedUrl'; -import { fileWithBase } from '../utils/base'; -import { version } from '../package.json'; - -interface Props { - data: CollectionEntry<'docs'>['data']; - lang: string; -} +import type { Props } from '../props'; -const { data, lang } = Astro.props; +const { entry, lang } = Astro.props; +const { data } = entry; const canonical = Astro.site ? new URL(Astro.url.pathname, Astro.site) : undefined; const description = data.description || config.description; @@ -94,7 +91,7 @@ if (config.social?.twitter) { tag: 'meta', attrs: { name: 'twitter:site', - content: new URL(config.social.twitter).pathname, + content: new URL(config.social.twitter.url).pathname, }, }); } diff --git a/packages/starlight/components/Header.astro b/packages/starlight/components/Header.astro index 6ed8aa1c20..51bf9a0834 100644 --- a/packages/starlight/components/Header.astro +++ b/packages/starlight/components/Header.astro @@ -1,24 +1,28 @@ --- -import LanguageSelect from './LanguageSelect.astro'; -import Search from './Search.astro'; -import SiteTitle from './SiteTitle.astro'; -import SocialIcons from './SocialIcons.astro'; -import ThemeSelect from './ThemeSelect.astro'; +import type { Props } from '../props'; -interface Props { - locale: string | undefined; -} - -const { locale } = Astro.props; +import { + LanguageSelect, + Search, + SiteTitle, + SocialIcons, + ThemeSelect, +} from 'virtual:starlight/components'; --- <div class="header sl-flex"> - <SiteTitle {locale} /> - <Search {locale} /> + <div class="sl-flex"> + <SiteTitle {...Astro.props} /> + </div> + <div class="sl-flex"> + <Search {...Astro.props} /> + </div> <div class="sl-hidden md:sl-flex right-group"> - <SocialIcons /> - <ThemeSelect {locale} /> - <LanguageSelect {locale} /> + <div class="sl-flex social-icons"> + <SocialIcons {...Astro.props} /> + </div> + <ThemeSelect {...Astro.props} /> + <LanguageSelect {...Astro.props} /> </div> </div> @@ -30,10 +34,16 @@ const { locale } = Astro.props; height: 100%; } - .right-group { + .right-group, + .social-icons { gap: 1rem; align-items: center; } + .social-icons::after { + content: ''; + height: 2rem; + border-inline-end: 1px solid var(--sl-color-gray-5); + } @media (min-width: 50rem) { :global(:root[data-has-sidebar]) { diff --git a/packages/starlight/components/Hero.astro b/packages/starlight/components/Hero.astro index fbc3692c94..bd9e8202fd 100644 --- a/packages/starlight/components/Hero.astro +++ b/packages/starlight/components/Hero.astro @@ -1,21 +1,18 @@ --- -import type { CollectionEntry } from 'astro:content'; import { Image } from 'astro:assets'; +import { PAGE_TITLE_ID } from '../constants'; +import type { Props } from '../props'; import CallToAction from './CallToAction.astro'; -interface Props { - fallbackTitle: string; - hero: NonNullable<CollectionEntry<'docs'>['data']['hero']>; -} - -const { title = Astro.props.fallbackTitle, tagline, image, actions } = Astro.props.hero; +const { data } = Astro.props.entry; +const { title = data.title, tagline, image, actions = [] } = data.hero || {}; const imageAttrs = { loading: 'eager' as const, decoding: 'async' as const, width: 400, height: 400, - alt: image?.alt, + alt: image?.alt || '', }; --- @@ -33,7 +30,7 @@ const imageAttrs = { } <div class="sl-flex stack"> <div class="sl-flex copy"> - <h1 id="_top" data-page-title set:html={title} /> + <h1 id={PAGE_TITLE_ID} data-page-title set:html={title} /> {tagline && <div class="tagline" set:html={tagline} />} </div> { diff --git a/packages/starlight/components/LanguageSelect.astro b/packages/starlight/components/LanguageSelect.astro index d59df1c18b..077935b3e5 100644 --- a/packages/starlight/components/LanguageSelect.astro +++ b/packages/starlight/components/LanguageSelect.astro @@ -3,10 +3,7 @@ import config from 'virtual:starlight/user-config'; import { localizedUrl } from '../utils/localizedUrl'; import { useTranslations } from '../utils/translations'; import Select from './Select.astro'; - -interface Props { - locale: string | undefined; -} +import type { Props } from '../props'; /** * Get the equivalent of the current page path for the passed locale. diff --git a/packages/starlight/components/LastUpdated.astro b/packages/starlight/components/LastUpdated.astro index 278057e905..73a6c2d02d 100644 --- a/packages/starlight/components/LastUpdated.astro +++ b/packages/starlight/components/LastUpdated.astro @@ -1,36 +1,17 @@ --- -import type { CollectionEntry } from 'astro:content'; -import { fileURLToPath } from 'node:url'; -import project from 'virtual:starlight/project-context'; -import { getFileCommitDate } from '../utils/git'; +import type { Props } from '../props'; import { useTranslations } from '../utils/translations'; -interface Props { - id: CollectionEntry<'docs'>['id']; - lang: string; - lastUpdated: Date | undefined; - locale: string | undefined; -} - -const { id, lang, lastUpdated, locale } = Astro.props; +const { lang, lastUpdated, locale } = Astro.props; const t = useTranslations(locale); - -const currentFilePath = fileURLToPath(new URL('src/content/docs/' + id, project.root)); - -let date = lastUpdated; -try { - if (!date) { - ({ date } = getFileCommitDate(currentFilePath, 'newest')); - } -} catch {} --- { - date && ( + lastUpdated && ( <p> {t('page.lastUpdated')}{' '} - <time datetime={date.toISOString()}> - {date.toLocaleDateString(lang, { dateStyle: 'medium' })} + <time datetime={lastUpdated.toISOString()}> + {lastUpdated.toLocaleDateString(lang, { dateStyle: 'medium' })} </time> </p> ) diff --git a/packages/starlight/components/MarkdownContent.astro b/packages/starlight/components/MarkdownContent.astro index ea44bd6c18..5e866a1ae7 100644 --- a/packages/starlight/components/MarkdownContent.astro +++ b/packages/starlight/components/MarkdownContent.astro @@ -1,3 +1,7 @@ +--- +import type { Props } from '../props'; +--- + <div class="content"><slot /></div> <style> diff --git a/packages/starlight/components/MobileMenuFooter.astro b/packages/starlight/components/MobileMenuFooter.astro new file mode 100644 index 0000000000..a5258da170 --- /dev/null +++ b/packages/starlight/components/MobileMenuFooter.astro @@ -0,0 +1,17 @@ +--- +import { LanguageSelect, ThemeSelect } from 'virtual:starlight/components'; +import type { Props } from '../props'; +--- + +<div class="mobile-preferences sl-flex"> + <ThemeSelect {...Astro.props} /> + <LanguageSelect {...Astro.props} /> +</div> + +<style> + .mobile-preferences { + justify-content: space-between; + border-top: 1px solid var(--sl-color-gray-6); + padding: 0.5rem 0; + } +</style> diff --git a/packages/starlight/components/MobileMenuToggle.astro b/packages/starlight/components/MobileMenuToggle.astro index a6e620b05d..e6619bbbaa 100644 --- a/packages/starlight/components/MobileMenuToggle.astro +++ b/packages/starlight/components/MobileMenuToggle.astro @@ -1,10 +1,8 @@ --- -import Icon from '../user-components/Icon.astro'; +import type { Props } from '../props'; import { useTranslations } from '../utils/translations'; -interface Props { - locale: string | undefined; -} +import Icon from '../user-components/Icon.astro'; const t = useTranslations(Astro.props.locale); --- diff --git a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro b/packages/starlight/components/MobileTableOfContents.astro similarity index 74% rename from packages/starlight/components/TableOfContents/MobileTableOfContents.astro rename to packages/starlight/components/MobileTableOfContents.astro index 34fe1d5e19..d5ec3cc672 100644 --- a/packages/starlight/components/TableOfContents/MobileTableOfContents.astro +++ b/packages/starlight/components/MobileTableOfContents.astro @@ -1,36 +1,33 @@ --- -import { useTranslations } from '../../utils/translations'; -import Icon from '../../user-components/Icon.astro'; -import TableOfContentsList from './TableOfContentsList.astro'; -import type { TocItem } from './generateToC'; +import { useTranslations } from '../utils/translations'; +import Icon from '../user-components/Icon.astro'; +import TableOfContentsList from './TableOfContents/TableOfContentsList.astro'; +import type { Props } from '../props'; -interface Props { - toc: TocItem[]; - locale: string | undefined; - maxHeadingLevel: number; - minHeadingLevel: number; -} - -const { locale, toc, maxHeadingLevel, minHeadingLevel } = Astro.props; +const { locale, toc } = Astro.props; const t = useTranslations(locale); --- -<mobile-starlight-toc data-min-h={minHeadingLevel} data-max-h={maxHeadingLevel}> - <nav aria-labelledby="starlight__on-this-page--mobile" class="lg:sl-hidden"> - <details id="starlight__mobile-toc"> - <summary id="starlight__on-this-page--mobile" class="sl-flex"> - <div class="toggle sl-flex"> - {t('tableOfContents.onThisPage')} - <Icon name={'right-caret'} class="caret" size="1rem" /> - </div> - <span class="display-current">{toc[0]?.text}</span> - </summary> - <div class="dropdown"> - <TableOfContentsList toc={toc} isMobile /> - </div> - </details> - </nav> -</mobile-starlight-toc> +{ + toc && ( + <mobile-starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}> + <nav aria-labelledby="starlight__on-this-page--mobile"> + <details id="starlight__mobile-toc"> + <summary id="starlight__on-this-page--mobile" class="sl-flex"> + <div class="toggle sl-flex"> + {t('tableOfContents.onThisPage')} + <Icon name={'right-caret'} class="caret" size="1rem" /> + </div> + <span class="display-current" /> + </summary> + <div class="dropdown"> + <TableOfContentsList toc={toc.items} isMobile /> + </div> + </details> + </nav> + </mobile-starlight-toc> + ) +} <style> nav { @@ -112,7 +109,7 @@ const t = useTranslations(locale); </style> <script> - import { StarlightTOC } from './starlight-toc'; + import { StarlightTOC } from './TableOfContents/starlight-toc'; class MobileStarlightTOC extends StarlightTOC { override set current(link: HTMLAnchorElement) { diff --git a/packages/starlight/components/Page.astro b/packages/starlight/components/Page.astro new file mode 100644 index 0000000000..306a0ed221 --- /dev/null +++ b/packages/starlight/components/Page.astro @@ -0,0 +1,120 @@ +--- +import type { Props } from '../props'; + +// Built-in CSS styles. +import '../style/props.css'; +import '../style/reset.css'; +import '../style/shiki.css'; +import '../style/util.css'; + +// Components — can override built-in CSS, but not user CSS. +import { + Banner, + ContentPanel, + PageTitle, + FallbackContentNotice, + Footer, + Header, + Head, + Hero, + MarkdownContent, + PageSidebar, + Sidebar, + SkipLink, + ThemeProvider, + PageFrame, + TwoColumnContent, +} from 'virtual:starlight/components'; + +// Remark component CSS (needs to override `MarkdownContent.astro`) +import '../style/asides.css'; + +// Important that this is the last import so it can override built-in styles. +import 'virtual:starlight/user-css'; + +const pagefindEnabled = + Astro.props.entry.slug !== '404' && + !Astro.props.entry.slug.endsWith('/404') && + Astro.props.entry.data.pagefind !== false; +--- + +<html + lang={Astro.props.lang} + dir={Astro.props.dir} + data-has-toc={Boolean(Astro.props.toc)} + data-has-sidebar={Astro.props.hasSidebar} + data-has-hero={Boolean(Astro.props.entry.data.hero)} +> + <head> + <Head {...Astro.props} /> + <style> + html:not([data-has-toc]) { + --sl-mobile-toc-height: 0rem; + } + html:not([data-has-sidebar]) { + --sl-content-width: 67.5rem; + } + /* Add scroll padding to ensure anchor headings aren't obscured by nav */ + html { + /* Additional padding is needed to account for the mobile TOC */ + scroll-padding-top: calc(1.5rem + var(--sl-nav-height) + var(--sl-mobile-toc-height)); + } + main { + padding-bottom: 3vh; + } + @media (min-width: 50em) { + [data-has-sidebar] { + --sl-content-inline-start: var(--sl-sidebar-width); + } + } + @media (min-width: 72em) { + html { + scroll-padding-top: calc(1.5rem + var(--sl-nav-height)); + } + } + </style> + <ThemeProvider {...Astro.props} /> + </head> + <body> + <SkipLink {...Astro.props} /> + <PageFrame {...Astro.props}> + <Header slot="header" {...Astro.props} /> + {Astro.props.hasSidebar && <Sidebar slot="sidebar" {...Astro.props} />} + <TwoColumnContent {...Astro.props}> + <PageSidebar slot="right-sidebar" {...Astro.props} /> + <main + data-pagefind-body={pagefindEnabled} + lang={Astro.props.entryMeta.lang} + dir={Astro.props.entryMeta.dir} + > + {/* TODO: Revisit how this logic flows. */} + <Banner {...Astro.props} /> + { + Astro.props.entry.data.hero ? ( + <ContentPanel {...Astro.props}> + <Hero {...Astro.props} /> + <MarkdownContent {...Astro.props}> + <slot /> + </MarkdownContent> + <Footer {...Astro.props} /> + </ContentPanel> + ) : ( + <> + <ContentPanel {...Astro.props}> + <PageTitle {...Astro.props} /> + {Astro.props.isFallback && <FallbackContentNotice {...Astro.props} />} + </ContentPanel> + <ContentPanel {...Astro.props}> + <MarkdownContent {...Astro.props}> + <slot /> + </MarkdownContent> + <Footer {...Astro.props} /> + </ContentPanel> + </> + ) + } + </main> + </TwoColumnContent> + </PageFrame> + </body> +</html> diff --git a/packages/starlight/layout/PageFrame.astro b/packages/starlight/components/PageFrame.astro similarity index 83% rename from packages/starlight/layout/PageFrame.astro rename to packages/starlight/components/PageFrame.astro index f0f1f7c9d9..86e98725f3 100644 --- a/packages/starlight/layout/PageFrame.astro +++ b/packages/starlight/components/PageFrame.astro @@ -1,11 +1,8 @@ --- -import MobileMenuToggle from '../components/MobileMenuToggle.astro'; +import type { Props } from '../props'; import { useTranslations } from '../utils/translations'; -interface Props { - hasSidebar: boolean; - locale: string | undefined; -} +import { MobileMenuToggle } from 'virtual:starlight/components'; const { hasSidebar, locale } = Astro.props; const t = useTranslations(locale); @@ -16,9 +13,9 @@ const t = useTranslations(locale); { hasSidebar && ( <nav class="sidebar" aria-label={t('sidebarNav.accessibleLabel')}> - <MobileMenuToggle {locale} /> + <MobileMenuToggle {...Astro.props} /> <div id="starlight__sidebar" class="sidebar-pane"> - <div class="sidebar-content"> + <div class="sidebar-content sl-flex"> <slot name="sidebar" /> </div> </div> @@ -60,6 +57,7 @@ const t = useTranslations(locale); padding-top: var(--sl-nav-height); width: 100%; background-color: var(--sl-color-black); + overflow-y: auto; } :global([aria-expanded='true']) ~ .sidebar-pane { @@ -68,7 +66,17 @@ const t = useTranslations(locale); .sidebar-content { height: 100%; - overflow-y: auto; + min-height: max-content; + padding: 1rem var(--sl-sidebar-pad-x) 0; + flex-direction: column; + gap: 1rem; + } + + @media (min-width: 50rem) { + .sidebar-content::after { + content: ''; + padding-bottom: 1px; + } } .main-frame { diff --git a/packages/starlight/components/RightSidebarPanel.astro b/packages/starlight/components/PageSidebar.astro similarity index 67% rename from packages/starlight/components/RightSidebarPanel.astro rename to packages/starlight/components/PageSidebar.astro index bc4244f21f..eab3d39134 100644 --- a/packages/starlight/components/RightSidebarPanel.astro +++ b/packages/starlight/components/PageSidebar.astro @@ -1,16 +1,28 @@ -<div class="right-sidebar-panel sl-hidden lg:sl-block"> - <div class="sl-container"> - <slot /> - </div> -</div> +--- +import type { Props } from '../props'; + +import { TableOfContents, MobileTableOfContents } from 'virtual:starlight/components'; +--- + +{ + Astro.props.toc && ( + <> + <div class="lg:sl-hidden"> + <MobileTableOfContents {...Astro.props} /> + </div> + <div class="right-sidebar-panel sl-hidden lg:sl-block"> + <div class="sl-container"> + <TableOfContents {...Astro.props} /> + </div> + </div> + </> + ) +} <style> .right-sidebar-panel { padding: 1rem var(--sl-sidebar-pad-x); } - .right-sidebar-panel + .right-sidebar-panel { - border-top: 1px solid var(--sl-color-hairline); - } .sl-container { width: calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x)); } diff --git a/packages/starlight/components/PageTitle.astro b/packages/starlight/components/PageTitle.astro new file mode 100644 index 0000000000..8c6d932b87 --- /dev/null +++ b/packages/starlight/components/PageTitle.astro @@ -0,0 +1,16 @@ +--- +import { PAGE_TITLE_ID } from '../constants'; +import type { Props } from '../props'; +--- + +<h1 id={PAGE_TITLE_ID}>{Astro.props.entry.data.title}</h1> + +<style> + h1 { + margin-top: 1rem; + font-size: var(--sl-text-h1); + line-height: var(--sl-line-height-headings); + font-weight: 600; + color: var(--sl-color-white); + } +</style> diff --git a/packages/starlight/components/PrevNextLinks.astro b/packages/starlight/components/Pagination.astro similarity index 86% rename from packages/starlight/components/PrevNextLinks.astro rename to packages/starlight/components/Pagination.astro index d18f1557f0..2b66ced592 100644 --- a/packages/starlight/components/PrevNextLinks.astro +++ b/packages/starlight/components/Pagination.astro @@ -1,16 +1,10 @@ --- -import type { Link } from '../utils/navigation'; import { useTranslations } from '../utils/translations'; import Icon from '../user-components/Icon.astro'; +import type { Props } from '../props'; -interface Props { - prev: Link | undefined; - next: Link | undefined; - dir: 'ltr' | 'rtl'; - locale: string | undefined; -} - -const { prev, next, dir, locale } = Astro.props; +const { dir, locale, pagination } = Astro.props; +const { prev, next } = pagination; const isRtl = dir === 'rtl'; const t = useTranslations(locale); --- diff --git a/packages/starlight/components/RightSidebar.astro b/packages/starlight/components/RightSidebar.astro deleted file mode 100644 index 142a7f0f51..0000000000 --- a/packages/starlight/components/RightSidebar.astro +++ /dev/null @@ -1,36 +0,0 @@ ---- -import type { MarkdownHeading } from 'astro'; -import RightSidebarPanel from './RightSidebarPanel.astro'; -import MobileTableOfContents from './TableOfContents/MobileTableOfContents.astro'; -import TableOfContents from './TableOfContents.astro'; -import { generateToC } from './TableOfContents/generateToC'; -import { useTranslations } from '../utils/translations'; - -interface Props { - headings: MarkdownHeading[]; - locale: string | undefined; - tocConfig: { maxHeadingLevel: number; minHeadingLevel: number } | false; -} - -const { headings, locale, tocConfig } = Astro.props; -const t = useTranslations(locale); -const tocProps = tocConfig && { - ...tocConfig, - locale, - toc: generateToC(headings, { - ...tocConfig, - title: t('tableOfContents.overview'), - }), -}; ---- - -{ - tocProps && ( - <> - <MobileTableOfContents {...tocProps} /> - <RightSidebarPanel> - <TableOfContents {...tocProps} /> - </RightSidebarPanel> - </> - ) -} diff --git a/packages/starlight/components/Search.astro b/packages/starlight/components/Search.astro index f5417b6012..e88e598620 100644 --- a/packages/starlight/components/Search.astro +++ b/packages/starlight/components/Search.astro @@ -2,10 +2,7 @@ import '@pagefind/default-ui/css/ui.css'; import { useTranslations } from '../utils/translations'; import Icon from '../user-components/Icon.astro'; - -interface Props { - locale: string | undefined; -} +import type { Props } from '../props'; const t = useTranslations(Astro.props.locale); const pagefindTranslations = { @@ -129,6 +126,9 @@ const pagefindTranslations = { </script> <style> + site-search { + display: contents; + } button[data-open-modal] { display: flex; align-items: center; diff --git a/packages/starlight/components/Sidebar.astro b/packages/starlight/components/Sidebar.astro index 7b898e91f1..00374ee084 100644 --- a/packages/starlight/components/Sidebar.astro +++ b/packages/starlight/components/Sidebar.astro @@ -1,42 +1,13 @@ --- -import type { getSidebar } from '../utils/navigation'; -import LanguageSelect from './LanguageSelect.astro'; -import SidebarSublist from './SidebarSublist.astro'; -import ThemeSelect from './ThemeSelect.astro'; +import type { Props } from '../props'; -interface Props { - sidebar: ReturnType<typeof getSidebar>; - locale: string | undefined; -} +import { MobileMenuFooter } from 'virtual:starlight/components'; +import SidebarSublist from './SidebarSublist.astro'; -const { sidebar, locale } = Astro.props; +const { sidebar } = Astro.props; --- -<div class="sidebar sl-flex"> - <SidebarSublist sublist={sidebar} /> - <div class="mobile-preferences sl-flex md:sl-hidden"> - <ThemeSelect {locale} /> - <LanguageSelect {locale} /> - </div> +<SidebarSublist sublist={sidebar} /> +<div class="md:sl-hidden"> + <MobileMenuFooter {...Astro.props} /> </div> - -<style> - .sidebar { - height: 100%; - padding: 1rem var(--sl-sidebar-pad-x); - flex-direction: column; - gap: 1rem; - } - - .mobile-preferences { - justify-content: space-between; - border-top: 1px solid var(--sl-color-gray-6); - padding: 0.5rem 0; - } - - @media (min-width: 50rem) { - .sidebar > :global(:nth-last-child(2)) { - padding-bottom: 1rem; - } - } -</style> diff --git a/packages/starlight/components/SiteTitle.astro b/packages/starlight/components/SiteTitle.astro index b4cf4c9064..bb1c20384e 100644 --- a/packages/starlight/components/SiteTitle.astro +++ b/packages/starlight/components/SiteTitle.astro @@ -2,26 +2,7 @@ import { logos } from 'virtual:starlight/user-images'; import config from 'virtual:starlight/user-config'; import { pathWithBase } from '../utils/base'; - -interface Props { - locale: string | undefined; -} - -if (config.logo) { - let err: string | undefined; - if ('src' in config.logo) { - if (!logos.dark || !logos.light) { - err = `Could not resolve logo import for "${config.logo.src}" (logo.src)`; - } - } else { - if (!logos.dark) { - err = `Could not resolve logo import for "${config.logo.dark}" (logo.dark)`; - } else if (!logos.light) { - err = `Could not resolve logo import for "${config.logo.light}" (logo.light)`; - } - } - if (err) throw new Error(err); -} +import type { Props } from '../props'; const href = pathWithBase(Astro.props.locale || '/'); --- diff --git a/packages/starlight/components/SkipLink.astro b/packages/starlight/components/SkipLink.astro index 3363dbe894..79de547c57 100644 --- a/packages/starlight/components/SkipLink.astro +++ b/packages/starlight/components/SkipLink.astro @@ -1,14 +1,12 @@ --- +import { PAGE_TITLE_ID } from '../constants'; import { useTranslations } from '../utils/translations'; - -interface Props { - locale: string | undefined; -} +import type { Props } from '../props'; const t = useTranslations(Astro.props.locale); --- -<a href="#_top">{t('skipLink.label')}</a> +<a href={`#${PAGE_TITLE_ID}`}>{t('skipLink.label')}</a> <style> a { diff --git a/packages/starlight/components/SocialIcons.astro b/packages/starlight/components/SocialIcons.astro index 4f5803a38c..75625255cc 100644 --- a/packages/starlight/components/SocialIcons.astro +++ b/packages/starlight/components/SocialIcons.astro @@ -1,49 +1,22 @@ --- import config from 'virtual:starlight/user-config'; import Icon from '../user-components/Icon.astro'; +import type { Props } from '../props'; type Platform = keyof NonNullable<typeof config.social>; - -const labels: Record<Platform, string> = { - github: 'GitHub', - gitlab: 'GitLab', - bitbucket: 'Bitbucket', - discord: 'Discord', - gitter: 'Gitter', - twitter: 'Twitter', - mastodon: 'Mastodon', - codeberg: 'Codeberg', - codePen: 'CodePen', - youtube: 'YouTube', - threads: 'Threads', - linkedin: 'LinkedIn', - twitch: 'Twitch', - microsoftTeams: 'Microsoft Teams', - instagram: 'Instagram', - stackOverflow: 'Stack Overflow', - 'x.com': 'X', - telegram: 'Telegram', - rss: 'RSS', - facebook: 'Facebook', - email: 'Email', -}; - -const links = Object.entries(config.social || {}).filter(([, url]) => Boolean(url)) as [ - platform: Platform, - url: string, -][]; +type SocialConfig = NonNullable<NonNullable<typeof config.social>[Platform]>; +const links = Object.entries(config.social || {}) as [Platform, SocialConfig][]; --- { links.length > 0 && ( <> - {links.map(([platform, url]) => ( + {links.map(([platform, { label, url }]) => ( <a href={url} rel="me" class="sl-flex"> - <span class="sr-only">{labels[platform]}</span> + <span class="sr-only">{label}</span> <Icon name={platform} /> </a> ))} - <div class="divider" /> </> ) } @@ -55,8 +28,4 @@ const links = Object.entries(config.social || {}).filter(([, url]) => Boolean(ur a:hover { opacity: 0.66; } - .divider { - height: 2rem; - border-inline-end: 1px solid var(--sl-color-gray-5); - } </style> diff --git a/packages/starlight/components/TableOfContents.astro b/packages/starlight/components/TableOfContents.astro index 570305af93..4b0f1d118b 100644 --- a/packages/starlight/components/TableOfContents.astro +++ b/packages/starlight/components/TableOfContents.astro @@ -1,24 +1,21 @@ --- import { useTranslations } from '../utils/translations'; import TableOfContentsList from './TableOfContents/TableOfContentsList.astro'; -import type { TocItem } from './TableOfContents/generateToC'; +import type { Props } from '../props'; -interface Props { - toc: TocItem[]; - locale: string | undefined; - maxHeadingLevel: number; - minHeadingLevel: number; -} - -const { locale, toc, maxHeadingLevel, minHeadingLevel } = Astro.props; +const { locale, toc } = Astro.props; const t = useTranslations(locale); --- -<starlight-toc data-min-h={minHeadingLevel} data-max-h={maxHeadingLevel}> - <nav aria-labelledby="starlight__on-this-page"> - <h2 id="starlight__on-this-page">{t('tableOfContents.onThisPage')}</h2> - <TableOfContentsList {toc} /> - </nav> -</starlight-toc> +{ + toc && ( + <starlight-toc data-min-h={toc.minHeadingLevel} data-max-h={toc.maxHeadingLevel}> + <nav aria-labelledby="starlight__on-this-page"> + <h2 id="starlight__on-this-page">{t('tableOfContents.onThisPage')}</h2> + <TableOfContentsList toc={toc.items} /> + </nav> + </starlight-toc> + ) +} <script src="./TableOfContents/starlight-toc"></script> diff --git a/packages/starlight/components/TableOfContents/TableOfContentsList.astro b/packages/starlight/components/TableOfContents/TableOfContentsList.astro index 7c290a1ef2..db370b6126 100644 --- a/packages/starlight/components/TableOfContents/TableOfContentsList.astro +++ b/packages/starlight/components/TableOfContents/TableOfContentsList.astro @@ -1,5 +1,5 @@ --- -import type { TocItem } from './generateToC'; +import type { TocItem } from '../../utils/generateToC'; interface Props { toc: TocItem[]; diff --git a/packages/starlight/components/TableOfContents/starlight-toc.ts b/packages/starlight/components/TableOfContents/starlight-toc.ts index 4283f36168..a5de15bd33 100644 --- a/packages/starlight/components/TableOfContents/starlight-toc.ts +++ b/packages/starlight/components/TableOfContents/starlight-toc.ts @@ -1,3 +1,5 @@ +import { PAGE_TITLE_ID } from '../../constants'; + export class StarlightTOC extends HTMLElement { private _current = this.querySelector('a[aria-current="true"]') as HTMLAnchorElement | null; private minH = parseInt(this.dataset.minH || '2', 10); @@ -20,7 +22,7 @@ export class StarlightTOC extends HTMLElement { const isHeading = (el: Element): el is HTMLHeadingElement => { if (el instanceof HTMLHeadingElement) { // Special case for page title h1 - if ('pageTitle' in el.dataset) return true; + if (el.id === PAGE_TITLE_ID) return true; // Check the heading level is within the user-configured limits for the ToC const level = el.tagName[1]; if (level) { diff --git a/packages/starlight/components/ThemeProvider.astro b/packages/starlight/components/ThemeProvider.astro index 8751721fa2..759ebc5f42 100644 --- a/packages/starlight/components/ThemeProvider.astro +++ b/packages/starlight/components/ThemeProvider.astro @@ -1,4 +1,5 @@ --- +import type { Props } from '../props'; import Icon from '../user-components/Icon.astro'; --- diff --git a/packages/starlight/components/ThemeSelect.astro b/packages/starlight/components/ThemeSelect.astro index 0146e09e8a..7d8e3fd425 100644 --- a/packages/starlight/components/ThemeSelect.astro +++ b/packages/starlight/components/ThemeSelect.astro @@ -1,10 +1,7 @@ --- import { useTranslations } from '../utils/translations'; import Select from './Select.astro'; - -interface Props { - locale: string | undefined; -} +import type { Props } from '../props'; const t = useTranslations(Astro.props.locale); --- diff --git a/packages/starlight/layout/TwoColumnContent.astro b/packages/starlight/components/TwoColumnContent.astro similarity index 93% rename from packages/starlight/layout/TwoColumnContent.astro rename to packages/starlight/components/TwoColumnContent.astro index 381632fb85..5eefb3e79a 100644 --- a/packages/starlight/layout/TwoColumnContent.astro +++ b/packages/starlight/components/TwoColumnContent.astro @@ -1,12 +1,10 @@ --- -interface Props { - hasToC: boolean; -} +import type { Props } from '../props'; --- <div class="lg:sl-flex"> { - Astro.props.hasToC && ( + Astro.props.toc && ( <aside class="right-sidebar-container"> <div class="right-sidebar"> <slot name="right-sidebar" /> diff --git a/packages/starlight/constants.ts b/packages/starlight/constants.ts new file mode 100644 index 0000000000..f679b6ae66 --- /dev/null +++ b/packages/starlight/constants.ts @@ -0,0 +1,4 @@ +// N.B. THIS FILE IS IMPORTED IN BOTH SERVER- AND CLIENT-SIDE CODE. +// THINK TWICE BEFORE ADDING STUFF AS IT WILL GET SHIPPED TO THE CLIENT. + +export const PAGE_TITLE_ID = '_top'; diff --git a/packages/starlight/index.astro b/packages/starlight/index.astro index d480f42069..65128ae829 100644 --- a/packages/starlight/index.astro +++ b/packages/starlight/index.astro @@ -1,8 +1,9 @@ --- import type { InferGetStaticPropsType } from 'astro'; +import { generateRouteData } from './utils/route-data'; import { paths } from './utils/routing'; -import Page from './layout/Page.astro'; +import Page from './components/Page.astro'; export async function getStaticPaths() { return paths; @@ -10,6 +11,7 @@ export async function getStaticPaths() { type Props = InferGetStaticPropsType<typeof getStaticPaths>; const { Content, headings } = await Astro.props.entry.render(); +const route = generateRouteData({ props: { ...Astro.props, headings }, url: Astro.url }); --- -<Page {...Astro.props} {headings}><Content /></Page> +<Page {...route}><Content /></Page> diff --git a/packages/starlight/integrations/virtual-user-config.ts b/packages/starlight/integrations/virtual-user-config.ts index 86c2b2a4b9..f8b8cd21b6 100644 --- a/packages/starlight/integrations/virtual-user-config.ts +++ b/packages/starlight/integrations/virtual-user-config.ts @@ -29,6 +29,9 @@ export function vitePluginStarlightUserConfig( opts.logo.light )}; export const logos = { dark, light };` : 'export const logos = {};', + 'virtual:starlight/components': Object.entries(opts.components) + .map(([name, path]) => `export { default as ${name} } from ${resolveId(path)};`) + .join(''), } satisfies Record<string, string>; /** Mapping names prefixed with `\0` to their original form. */ diff --git a/packages/starlight/layout/Page.astro b/packages/starlight/layout/Page.astro deleted file mode 100644 index 0999122dff..0000000000 --- a/packages/starlight/layout/Page.astro +++ /dev/null @@ -1,132 +0,0 @@ ---- -import config from 'virtual:starlight/user-config'; -import type { MarkdownHeading } from 'astro'; -import { getSidebar } from '../utils/navigation'; -import type { Route } from '../utils/routing'; - -// Built-in CSS styles. -import '../style/props.css'; -import '../style/reset.css'; -import '../style/shiki.css'; -import '../style/util.css'; - -// Components — can override built-in CSS, but not user CSS. -import ContentPanel from '../components/ContentPanel.astro'; -import FallbackContentNotice from '../components/FallbackContentNotice.astro'; -import Footer from '../components/Footer.astro'; -import HeadSEO from '../components/HeadSEO.astro'; -import Header from '../components/Header.astro'; -import Hero from '../components/Hero.astro'; -import MarkdownContent from '../components/MarkdownContent.astro'; -import RightSidebar from '../components/RightSidebar.astro'; -import Sidebar from '../components/Sidebar.astro'; -import SkipLink from '../components/SkipLink.astro'; -import ThemeProvider from '../components/ThemeProvider.astro'; -import PageFrame from '../layout/PageFrame.astro'; -import TwoColumnContent from '../layout/TwoColumnContent.astro'; -import Banner from '../components/Banner.astro'; - -// Remark component CSS (needs to override `MarkdownContent.astro`) -import '../style/asides.css'; - -// Important that this is the last import so it can override built-in styles. -import 'virtual:starlight/user-css'; - -type Props = Route & { headings: MarkdownHeading[] }; - -const { dir, entry, entryMeta, headings, isFallback, lang, locale } = Astro.props; -const sidebar = getSidebar(Astro.url.pathname, locale); - -const hasSidebar = entry.data.template !== 'splash'; -const tocConfig = !hasSidebar - ? false - : entry.data.tableOfContents !== undefined - ? entry.data.tableOfContents - : config.tableOfContents; -const hasToC = Boolean(tocConfig); -const hasHero = Boolean(entry.data.hero); -const pagefindEnabled = - entry.slug !== '404' && !entry.slug.endsWith('/404') && entry.data.pagefind !== false; ---- - -<html - lang={lang} - dir={dir} - data-has-toc={hasToC} - data-has-sidebar={hasSidebar} - data-has-hero={hasHero} -> - <head> - <HeadSEO data={entry.data} lang={lang} /> - <style> - html:not([data-has-toc]) { - --sl-mobile-toc-height: 0rem; - } - html:not([data-has-sidebar]) { - --sl-content-width: 67.5rem; - } - /* Add scroll padding to ensure anchor headings aren't obscured by nav */ - html { - /* Additional padding is needed to account for the mobile TOC */ - scroll-padding-top: calc(1.5rem + var(--sl-nav-height) + var(--sl-mobile-toc-height)); - } - main { - padding-bottom: 3vh; - } - @media (min-width: 50em) { - [data-has-sidebar] { - --sl-content-inline-start: var(--sl-sidebar-width); - } - } - @media (min-width: 72em) { - html { - scroll-padding-top: calc(1.5rem + var(--sl-nav-height)); - } - } - </style> - </head> - <body> - <ThemeProvider /> - <SkipLink {locale} /> - <PageFrame {locale} {hasSidebar}> - <Header slot="header" {locale} /> - {hasSidebar && <Sidebar slot="sidebar" {sidebar} {locale} />} - <TwoColumnContent {hasToC}> - <RightSidebar slot="right-sidebar" {headings} {locale} {tocConfig} /> - <main data-pagefind-body={pagefindEnabled} lang={entryMeta.lang} dir={entryMeta.dir}> - {/* TODO: Revisit how this logic flows. */} - {entry.data.banner && <Banner {...entry.data.banner} />} - { - entry.data.hero ? ( - <ContentPanel> - <Hero hero={entry.data.hero} fallbackTitle={entry.data.title} /> - <MarkdownContent> - <slot /> - </MarkdownContent> - </ContentPanel> - ) : ( - <> - <ContentPanel> - <h1 - id="_top" - data-page-title - style="font-size: var(--sl-text-h1); line-height: var(--sl-line-height-headings); font-weight: 600; color: var(--sl-color-white); margin-top: 1rem;" - > - {entry.data.title} - </h1> - {isFallback && <FallbackContentNotice {locale} />} - </ContentPanel> - <ContentPanel> - <MarkdownContent> - <slot /> - </MarkdownContent> - <Footer {...{ entry, dir, lang, locale, sidebar }} /> - </ContentPanel> - </> - ) - } - </main> - </TwoColumnContent> - </PageFrame> - </body> -</html> diff --git a/packages/starlight/package.json b/packages/starlight/package.json index dc9f928a74..f301e36f43 100644 --- a/packages/starlight/package.json +++ b/packages/starlight/package.json @@ -26,6 +26,135 @@ "exports": { ".": "./index.ts", "./components": "./components.ts", + "./components/Badge.astro": { + "types": "./components/Badge.astro.tsx", + "import": "./components/Badge.astro" + }, + "./components/LanguageSelect.astro": { + "types": "./components/LanguageSelect.astro.tsx", + "import": "./components/LanguageSelect.astro" + }, + "./components/Select.astro": { + "types": "./components/Select.astro.tsx", + "import": "./components/Select.astro" + }, + "./components/Banner.astro": { + "types": "./components/Banner.astro.tsx", + "import": "./components/Banner.astro" + }, + "./components/LastUpdated.astro": { + "types": "./components/LastUpdated.astro.tsx", + "import": "./components/LastUpdated.astro" + }, + "./components/Sidebar.astro": { + "types": "./components/Sidebar.astro.tsx", + "import": "./components/Sidebar.astro" + }, + "./components/CallToAction.astro": { + "types": "./components/CallToAction.astro.tsx", + "import": "./components/CallToAction.astro" + }, + "./components/MarkdownContent.astro": { + "types": "./components/MarkdownContent.astro.tsx", + "import": "./components/MarkdownContent.astro" + }, + "./components/SidebarSublist.astro": { + "types": "./components/SidebarSublist.astro.tsx", + "import": "./components/SidebarSublist.astro" + }, + "./components/ContentPanel.astro": { + "types": "./components/ContentPanel.astro.tsx", + "import": "./components/ContentPanel.astro" + }, + "./components/MobileMenuFooter.astro": { + "types": "./components/MobileMenuFooter.astro.tsx", + "import": "./components/MobileMenuFooter.astro" + }, + "./components/SiteTitle.astro": { + "types": "./components/SiteTitle.astro.tsx", + "import": "./components/SiteTitle.astro" + }, + "./components/EditLink.astro": { + "types": "./components/EditLink.astro.tsx", + "import": "./components/EditLink.astro" + }, + "./components/MobileMenuToggle.astro": { + "types": "./components/MobileMenuToggle.astro.tsx", + "import": "./components/MobileMenuToggle.astro" + }, + "./components/SkipLink.astro": { + "types": "./components/SkipLink.astro.tsx", + "import": "./components/SkipLink.astro" + }, + "./components/MobileTableOfContents.astro": { + "types": "./components/MobileTableOfContents.astro.tsx", + "import": "./components/MobileTableOfContents.astro" + }, + "./components/SocialIcons.astro": { + "types": "./components/SocialIcons.astro.tsx", + "import": "./components/SocialIcons.astro" + }, + "./components/FallbackContentNotice.astro": { + "types": "./components/FallbackContentNotice.astro.tsx", + "import": "./components/FallbackContentNotice.astro" + }, + "./components/Page.astro": { + "types": "./components/Page.astro.tsx", + "import": "./components/Page.astro" + }, + "./components/Footer.astro": { + "types": "./components/Footer.astro.tsx", + "import": "./components/Footer.astro" + }, + "./components/PageFrame.astro": { + "types": "./components/PageFrame.astro.tsx", + "import": "./components/PageFrame.astro" + }, + "./components/TableOfContents.astro": { + "types": "./components/TableOfContents.astro.tsx", + "import": "./components/TableOfContents.astro" + }, + "./components/Head.astro": { + "types": "./components/Head.astro.tsx", + "import": "./components/Head.astro" + }, + "./components/PageSidebar.astro": { + "types": "./components/PageSidebar.astro.tsx", + "import": "./components/PageSidebar.astro" + }, + "./components/ThemeProvider.astro": { + "types": "./components/ThemeProvider.astro.tsx", + "import": "./components/ThemeProvider.astro" + }, + "./components/Header.astro": { + "types": "./components/Header.astro.tsx", + "import": "./components/Header.astro" + }, + "./components/PageTitle.astro": { + "types": "./components/PageTitle.astro.tsx", + "import": "./components/PageTitle.astro" + }, + "./components/ThemeSelect.astro": { + "types": "./components/ThemeSelect.astro.tsx", + "import": "./components/ThemeSelect.astro" + }, + "./components/Hero.astro": { + "types": "./components/Hero.astro.tsx", + "import": "./components/Hero.astro" + }, + "./components/Pagination.astro": { + "types": "./components/Pagination.astro.tsx", + "import": "./components/Pagination.astro" + }, + "./components/TwoColumnContent.astro": { + "types": "./components/TwoColumnContent.astro.tsx", + "import": "./components/TwoColumnContent.astro" + }, + "./components/Search.astro": { + "types": "./components/Search.astro.tsx", + "import": "./components/Search.astro" + }, + "./props": "./props.ts", "./schema": "./schema.ts", "./types": "./types.ts", "./index.astro": "./index.astro", diff --git a/packages/starlight/props.ts b/packages/starlight/props.ts new file mode 100644 index 0000000000..76c119e40e --- /dev/null +++ b/packages/starlight/props.ts @@ -0,0 +1 @@ +export type { StarlightRouteData as Props } from './utils/route-data'; diff --git a/packages/starlight/schemas/components.ts b/packages/starlight/schemas/components.ts new file mode 100644 index 0000000000..262530313f --- /dev/null +++ b/packages/starlight/schemas/components.ts @@ -0,0 +1,256 @@ +import { z } from 'astro/zod'; + +export function ComponentConfigSchema() { + return z + .object({ + /* + HEAD ---------------------------------------------------------------------------------------- + */ + + /** + * Component rendered inside each page’s `<head>`. + * Includes important tags including `<title>`, and `<meta charset="utf-8">`. + * + * Override this component as a last resort. Prefer the `head` option Starlight config if possible. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Head.astro `Head` default implementation} + */ + Head: z.string().default('@astrojs/starlight/components/Head.astro'), + + /** + * Component rendered inside `<head>` that sets up dark/light theme support. + * The default implementation includes an inline script and a `<template>` used by the + * script in `ThemeSelect.astro`. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeProvider.astro `ThemeProvider` default implementation} + */ + ThemeProvider: z.string().default('@astrojs/starlight/components/ThemeProvider.astro'), + + /* + BODY ---------------------------------------------------------------------------------------- + */ + + /** + * Component rendered as the first element inside `<body>` which links to the main page + * content for accessibility. The default implementation is hidden until a user focuses it + * by tabbing with their keyboard. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/SkipLink.astro `SkipLink` default implementation} + */ + SkipLink: z.string().default('@astrojs/starlight/components/SkipLink.astro'), + + /* + LAYOUT -------------------------------------------------------------------------------------- + */ + + /** + * Layout component wrapped around most of the page content. + * The default implementation sets up the header–sidebar–main layout and includes + * `header` and `sidebar` named slots along with a default slot for the main content. + * It also renders `<MobileMenuToggle />` to support toggling the sidebar navigation + * on small (mobile) viewports. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageFrame.astro `PageFrame` default implementation} + */ + PageFrame: z.string().default('@astrojs/starlight/components/PageFrame.astro'), + /** + * Component rendered inside `<PageFrame>` that is responsible for toggling the + * sidebar navigation on small (mobile) viewports. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuToggle.astro `MobileMenuToggle` default implementation} + */ + MobileMenuToggle: z.string().default('@astrojs/starlight/components/MobileMenuToggle.astro'), + + /** + * Layout component wrapped around the main content column and right sidebar (table of contents). + * The default implementation handles the switch between a single-column, small-viewport layout + * and a two-column, larger-viewport layout. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/TwoColumnContent.astro `TwoColumnContent` default implementation} + */ + TwoColumnContent: z.string().default('@astrojs/starlight/components/TwoColumnContent.astro'), + + /* + HEADER -------------------------------------------------------------------------------------- + */ + + /** + * Header component displayed at the top of every page. + * The default implementation displays `<SiteTitle />`, `<Search />`, `<SocialIcons />`, + * `<ThemeSelect />`, and `<LanguageSelect />`. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Header.astro `Header` default implementation} + */ + Header: z.string().default('@astrojs/starlight/components/Header.astro'), + /** + * Component rendered at the start of the site header to render the site title. + * The default implementation includes logic for rendering logos defined in Starlight config. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/SiteTitle.astro `SiteTitle` default implementation} + */ + SiteTitle: z.string().default('@astrojs/starlight/components/SiteTitle.astro'), + /** + * Component used to render Starlight’s search UI. The default implementation includes the + * button in the header and the code for displaying a search modal when it is clicked. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro `Search` default implementation} + */ + Search: z.string().default('@astrojs/starlight/components/Search.astro'), + /** + * Component rendered in the site header including social icon links. The default + * implementation uses the `social` option in Starlight config to render icons and links. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/SocialIcons.astro `SocialIcons` default implementation} + */ + SocialIcons: z.string().default('@astrojs/starlight/components/SocialIcons.astro'), + /** + * Component rendered in the site header that allows users to select their preferred color scheme. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/ThemeSelect.astro `ThemeSelect` default implementation} + */ + ThemeSelect: z.string().default('@astrojs/starlight/components/ThemeSelect.astro'), + /** + * Component rendered in the site header that allows users to switch to a different language. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/LanguageSelect.astro `LanguageSelect` default implementation} + */ + LanguageSelect: z.string().default('@astrojs/starlight/components/LanguageSelect.astro'), + + /* + SIDEBAR ------------------------------------------------------------------------------------- + */ + + /** + * Component rendered before page content that contains global navigation. + * The default implementation displays as a sidebar on wide enough viewports and inside a + * drop-down menu on small (mobile) viewports. It also renders `<MobileMenuFooter />` to + * show additional items inside the mobile menu. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Sidebar.astro `Sidebar` default implementation} + */ + Sidebar: z.string().default('@astrojs/starlight/components/Sidebar.astro'), + /** + * Component rendered at the bottom of the mobile drop-down menu. + * The default implementation renders `<ThemeSelect />` and `<LanguageSelect />`. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileMenuFooter.astro `MobileMenuFooter` default implementation} + */ + MobileMenuFooter: z.string().default('@astrojs/starlight/components/MobileMenuFooter.astro'), + + /* + TOC ----------------------------------------------------------------------------------------- + */ + + /** + * Component rendered before the main page’s content to display a table of contents. + * The default implementation renders `<TableOfContents />` and `<MobileTableOfContents />`. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageSidebar.astro `PageSidebar` default implementation} + */ + PageSidebar: z.string().default('@astrojs/starlight/components/PageSidebar.astro'), + /** + * Component that renders the current page’s table of contents on wider viewports. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/TableOfContents.astro `TableOfContents` default implementation} + */ + TableOfContents: z.string().default('@astrojs/starlight/components/TableOfContents.astro'), + /** + * Component that renders the current page’s table of contents on small (mobile) viewports. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MobileTableOfContents.astro `MobileTableOfContents` default implementation} + */ + MobileTableOfContents: z + .string() + .default('@astrojs/starlight/components/MobileTableOfContents.astro'), + + /* + CONTENT HEADER ------------------------------------------------------------------------------ + */ + + /** + * Banner component rendered at the top of each page. The default implementation uses the + * page’s `banner` frontmatter value to decide whether or not to render. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Banner.astro `Banner` default implementation} + */ + Banner: z.string().default('@astrojs/starlight/components/Banner.astro'), + + /** + * Layout component used to wrap sections of the main content column. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/ContentPanel.astro `ContentPanel` default implementation} + */ + ContentPanel: z.string().default('@astrojs/starlight/components/ContentPanel.astro'), + + /** + * Component containing the `<h1>` element for the current page. + * + * Implementations should ensure they set `id="_top"` on the `<h1>` element as in the default + * implementation. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/PageTitle.astro `PageTitle` default implementation} + */ + PageTitle: z.string().default('@astrojs/starlight/components/PageTitle.astro'), + + /** + * Notice displayed to users on pages where a translation for the current language is not + * available. Only used on multilingual sites. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/FallbackContentNotice.astro `FallbackContentNotice` default implementation} + */ + FallbackContentNotice: z + .string() + .default('@astrojs/starlight/components/FallbackContentNotice.astro'), + + /** + * Component rendered at the top of the page when `hero` is set in frontmatter. The default + * implementation shows a large title, tagline, and call-to-action links alongside an optional image. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Hero.astro `Hero` default implementation} + */ + Hero: z.string().default('@astrojs/starlight/components/Hero.astro'), + + /* + CONTENT ------------------------------------------------------------------------------------- + */ + + /** + * Component rendered around each page’s main content. + * The default implementation sets up basic styles to apply to Markdown content. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/MarkdownContent.astro `MarkdownContent` default implementation} + */ + MarkdownContent: z.string().default('@astrojs/starlight/components/MarkdownContent.astro'), + + /* + CONTENT FOOTER ------------------------------------------------------------------------------ + */ + + /** + * Footer component displayed at the bottom of each documentation page. + * The default implementation displays `<LastUpdated />`, `<Pagination />`, and `<EditLink />`. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Footer.astro `Footer` default implementation} + */ + Footer: z.string().default('@astrojs/starlight/components/Footer.astro'), + /** + * Component rendered in the page footer to display the last-updated date. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/LastUpdated.astro `LastUpdated` default implementation} + */ + LastUpdated: z.string().default('@astrojs/starlight/components/LastUpdated.astro'), + /** + * Component rendered in the page footer to display navigation arrows between previous/next pages. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/Pagination.astro `Pagination` default implementation} + */ + Pagination: z.string().default('@astrojs/starlight/components/Pagination.astro'), + /** + * Component rendered in the page footer to display a link to where the page can be edited. + * + * @see {@link https://github.com/withastro/starlight/blob/main/packages/starlight/components/EditLink.astro `EditLink` default implementation} + */ + EditLink: z.string().default('@astrojs/starlight/components/EditLink.astro'), + }) + .default({}); +} diff --git a/packages/starlight/schemas/social.ts b/packages/starlight/schemas/social.ts new file mode 100644 index 0000000000..f199e76618 --- /dev/null +++ b/packages/starlight/schemas/social.ts @@ -0,0 +1,65 @@ +import { z } from 'astro/zod'; + +export const SocialLinksSchema = () => + z + .record( + z.enum([ + 'twitter', + 'mastodon', + 'github', + 'gitlab', + 'bitbucket', + 'discord', + 'gitter', + 'codeberg', + 'codePen', + 'youtube', + 'threads', + 'linkedin', + 'twitch', + 'microsoftTeams', + 'instagram', + 'stackOverflow', + 'x.com', + 'telegram', + 'rss', + 'facebook', + 'email', + ]), + // Link to the respective social profile for this site + z.string().url() + ) + .transform((links) => { + const labelledLinks: Partial<Record<keyof typeof links, { label: string; url: string }>> = {}; + for (const _k in links) { + const key = _k as keyof typeof links; + const url = links[key]; + if (!url) continue; + const label = { + github: 'GitHub', + gitlab: 'GitLab', + bitbucket: 'Bitbucket', + discord: 'Discord', + gitter: 'Gitter', + twitter: 'Twitter', + mastodon: 'Mastodon', + codeberg: 'Codeberg', + codePen: 'CodePen', + youtube: 'YouTube', + threads: 'Threads', + linkedin: 'LinkedIn', + twitch: 'Twitch', + microsoftTeams: 'Microsoft Teams', + instagram: 'Instagram', + stackOverflow: 'Stack Overflow', + 'x.com': 'X', + telegram: 'Telegram', + rss: 'RSS', + facebook: 'Facebook', + email: 'Email', + }[key]; + labelledLinks[key] = { label, url }; + } + return labelledLinks; + }) + .optional(); diff --git a/packages/starlight/components/TableOfContents/generateToC.ts b/packages/starlight/utils/generateToC.ts similarity index 86% rename from packages/starlight/components/TableOfContents/generateToC.ts rename to packages/starlight/utils/generateToC.ts index 73c08e3d47..b961ad54e1 100644 --- a/packages/starlight/components/TableOfContents/generateToC.ts +++ b/packages/starlight/utils/generateToC.ts @@ -1,4 +1,5 @@ import type { MarkdownHeading } from 'astro'; +import { PAGE_TITLE_ID } from '../constants'; export interface TocItem extends MarkdownHeading { children: TocItem[]; @@ -16,7 +17,7 @@ export function generateToC( { minHeadingLevel, maxHeadingLevel, title }: TocOpts ) { headings = headings.filter(({ depth }) => depth >= minHeadingLevel && depth <= maxHeadingLevel); - const toc: Array<TocItem> = [{ depth: 2, slug: '_top', text: title, children: [] }]; + const toc: Array<TocItem> = [{ depth: 2, slug: PAGE_TITLE_ID, text: title, children: [] }]; for (const heading of headings) injectChild(toc, { ...heading, children: [] }); return toc; } diff --git a/packages/starlight/utils/navigation.ts b/packages/starlight/utils/navigation.ts index a8af46b501..c8a45b4ade 100644 --- a/packages/starlight/utils/navigation.ts +++ b/packages/starlight/utils/navigation.ts @@ -287,7 +287,9 @@ export function getPrevNextLinks( next?: PrevNextLinkConfig; } ): { + /** Link to previous page in the sidebar. */ prev: Link | undefined; + /** Link to next page in the sidebar. */ next: Link | undefined; } { const entries = flattenSidebar(sidebar); diff --git a/packages/starlight/utils/route-data.ts b/packages/starlight/utils/route-data.ts new file mode 100644 index 0000000000..ae204f8053 --- /dev/null +++ b/packages/starlight/utils/route-data.ts @@ -0,0 +1,97 @@ +import type { MarkdownHeading } from 'astro'; +import { fileURLToPath } from 'node:url'; +import project from 'virtual:starlight/project-context'; +import config from 'virtual:starlight/user-config'; +import { generateToC, type TocItem } from './generateToC'; +import { getFileCommitDate } from './git'; +import { getPrevNextLinks, getSidebar, type SidebarEntry } from './navigation'; +import type { Route } from './routing'; +import { useTranslations } from './translations'; +import { ensureTrailingSlash } from './path'; + +interface PageProps extends Route { + headings: MarkdownHeading[]; +} + +export interface StarlightRouteData extends Route { + /** Array of Markdown headings extracted from the current page. */ + headings: MarkdownHeading[]; + /** Site navigation sidebar entries for this page. */ + sidebar: SidebarEntry[]; + /** Whether or not the sidebar should be displayed on this page. */ + hasSidebar: boolean; + /** Links to the previous and next page in the sidebar if enabled. */ + pagination: ReturnType<typeof getPrevNextLinks>; + /** Table of contents for this page if enabled. */ + toc: { minHeadingLevel: number; maxHeadingLevel: number; items: TocItem[] } | undefined; + /** JS Date object representing when this page was last updated if enabled. */ + lastUpdated: Date | undefined; + /** URL object for the address where this page can be edited if enabled. */ + editUrl: URL | undefined; +} + +export function generateRouteData({ + props, + url, +}: { + props: PageProps; + url: URL; +}): StarlightRouteData { + const { entry, locale } = props; + const sidebar = getSidebar(url.pathname, locale); + return { + ...props, + sidebar, + hasSidebar: entry.data.template !== 'splash', + pagination: getPrevNextLinks(sidebar, config.pagination, entry.data), + toc: getToC(props), + lastUpdated: getLastUpdated(props), + editUrl: getEditUrl(props), + }; +} + +function getToC({ entry, locale, headings }: PageProps) { + const tocConfig = + entry.data.template === 'splash' + ? false + : entry.data.tableOfContents !== undefined + ? entry.data.tableOfContents + : config.tableOfContents; + if (!tocConfig) return; + const t = useTranslations(locale); + return { + ...tocConfig, + items: generateToC(headings, { ...tocConfig, title: t('tableOfContents.overview') }), + }; +} + +function getLastUpdated({ entry, id }: PageProps): Date | undefined { + if (entry.data.lastUpdated ?? config.lastUpdated) { + const currentFilePath = fileURLToPath(new URL('src/content/docs/' + id, project.root)); + let date = typeof entry.data.lastUpdated !== 'boolean' ? entry.data.lastUpdated : undefined; + if (!date) { + try { + ({ date } = getFileCommitDate(currentFilePath, 'newest')); + } catch {} + } + return date; + } + return; +} + +function getEditUrl({ entry, id }: PageProps): URL | undefined { + const { editUrl } = entry.data; + // If frontmatter value is false, editing is disabled for this page. + if (editUrl === false) return; + + let url: string | undefined; + if (typeof editUrl === 'string') { + // If a URL was provided in frontmatter, use that. + url = editUrl; + } else if (config.editLink.baseUrl) { + const srcPath = project.srcDir.replace(project.root, ''); + // If a base URL was added in Starlight config, synthesize the edit URL from it. + url = ensureTrailingSlash(config.editLink.baseUrl) + srcPath + 'content/docs/' + id; + } + return url ? new URL(url) : undefined; +} diff --git a/packages/starlight/utils/routing.ts b/packages/starlight/utils/routing.ts index 496578ce20..5b0b7091d9 100644 --- a/packages/starlight/utils/routing.ts +++ b/packages/starlight/utils/routing.ts @@ -8,16 +8,26 @@ import { slugToLocaleData, slugToParam, } from './slugs'; +import { validateLogoImports } from './validateLogoImports'; + +// Validate any user-provided logos imported correctly. +// We do this here so all pages trigger it and at the top level so it runs just once. +validateLogoImports(); export type StarlightDocsEntry = Omit<CollectionEntry<'docs'>, 'slug'> & { slug: string; }; export interface Route extends LocaleData { + /** Content collection entry for the current page. Includes frontmatter at `data`. */ entry: StarlightDocsEntry; + /** Locale metadata for the page content. Can be different from top-level locale values when a page is using fallback content. */ entryMeta: LocaleData; + /** The slug, a.k.a. permalink, for this page. */ slug: string; + /** The unique ID for this page. */ id: string; + /** True if this page is untranslated in the current language and using fallback content from the default locale. */ isFallback?: true; [key: string]: unknown; } diff --git a/packages/starlight/utils/user-config.ts b/packages/starlight/utils/user-config.ts index 4cb0129318..03e991f630 100644 --- a/packages/starlight/utils/user-config.ts +++ b/packages/starlight/utils/user-config.ts @@ -1,10 +1,13 @@ import { z } from 'astro/zod'; import { parse as bcpParse, stringify as bcpStringify } from 'bcp-47'; +import { BadgeConfigSchema } from '../schemas/badge'; +import { ComponentConfigSchema } from '../schemas/components'; +import { FaviconSchema } from '../schemas/favicon'; import { HeadConfigSchema } from '../schemas/head'; import { LogoConfigSchema } from '../schemas/logo'; -import { TableOfContentsSchema } from '../schemas/tableOfContents'; -import { FaviconSchema } from '../schemas/favicon'; import { SidebarItemSchema } from '../schemas/sidebar'; +import { SocialLinksSchema } from '../schemas/social'; +import { TableOfContentsSchema } from '../schemas/tableOfContents'; const LocaleSchema = z.object({ /** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */ @@ -60,35 +63,7 @@ const UserConfigSchema = z.object({ * youtube: 'https://youtube.com/@astrodotbuild', * } */ - social: z - .record( - z.enum([ - 'twitter', - 'mastodon', - 'github', - 'gitlab', - 'bitbucket', - 'discord', - 'gitter', - 'codeberg', - 'codePen', - 'youtube', - 'threads', - 'linkedin', - 'twitch', - 'microsoftTeams', - 'instagram', - 'stackOverflow', - 'x.com', - 'telegram', - 'rss', - 'facebook', - 'email', - ]), - // Link to the respective social profile for this site - z.string().url() - ) - .optional(), + social: SocialLinksSchema(), /** The tagline for your website. */ tagline: z.string().optional().describe('The tagline for your website.'), @@ -209,6 +184,9 @@ const UserConfigSchema = z.object({ /** The default favicon for your site which should be a path to an image in the `public/` directory. */ favicon: FaviconSchema(), + /** Specify paths to components that should override Starlight’s default components */ + components: ComponentConfigSchema(), + /** Will be used as title delimiter in the generated `<title>` tag. */ titleDelimiter: z .string() diff --git a/packages/starlight/utils/validateLogoImports.ts b/packages/starlight/utils/validateLogoImports.ts new file mode 100644 index 0000000000..773fae9812 --- /dev/null +++ b/packages/starlight/utils/validateLogoImports.ts @@ -0,0 +1,21 @@ +import config from 'virtual:starlight/user-config'; +import { logos } from 'virtual:starlight/user-images'; + +/** Check user-imported logo images have resolved correctly. */ +export function validateLogoImports(): void { + if (config.logo) { + let err: string | undefined; + if ('src' in config.logo) { + if (!logos.dark || !logos.light) { + err = `Could not resolve logo import for "${config.logo.src}" (logo.src)`; + } + } else { + if (!logos.dark) { + err = `Could not resolve logo import for "${config.logo.dark}" (logo.dark)`; + } else if (!logos.light) { + err = `Could not resolve logo import for "${config.logo.light}" (logo.light)`; + } + } + if (err) throw new Error(err); + } +} diff --git a/packages/starlight/virtual.d.ts b/packages/starlight/virtual.d.ts index 1230c46053..f448489c93 100644 --- a/packages/starlight/virtual.d.ts +++ b/packages/starlight/virtual.d.ts @@ -15,3 +15,40 @@ declare module 'virtual:starlight/user-images' { light?: ImageMetadata; }; } + +declare module 'virtual:starlight/components' { + export const Banner: typeof import('./components/Banner.astro').default; + export const ContentPanel: typeof import('./components/ContentPanel.astro').default; + export const PageTitle: typeof import('./components/PageTitle.astro').default; + export const FallbackContentNotice: typeof import('./components/FallbackContentNotice.astro').default; + + export const Footer: typeof import('./components/Footer.astro').default; + export const LastUpdated: typeof import('./components/LastUpdated.astro').default; + export const Pagination: typeof import('./components/Pagination.astro').default; + export const EditLink: typeof import('./components/EditLink.astro').default; + + export const Header: typeof import('./components/Header.astro').default; + export const LanguageSelect: typeof import('./components/LanguageSelect.astro').default; + export const Search: typeof import('./components/Search.astro').default; + export const SiteTitle: typeof import('./components/SiteTitle.astro').default; + export const SocialIcons: typeof import('./components/SocialIcons.astro').default; + export const ThemeSelect: typeof import('./components/ThemeSelect.astro').default; + + export const Head: typeof import('./components/Head.astro').default; + export const Hero: typeof import('./components/Hero.astro').default; + export const MarkdownContent: typeof import('./components/MarkdownContent.astro').default; + + export const PageSidebar: typeof import('./components/PageSidebar.astro').default; + export const TableOfContents: typeof import('./components/TableOfContents.astro').default; + export const MobileTableOfContents: typeof import('./components/MobileTableOfContents.astro').default; + + export const Sidebar: typeof import('./components/Sidebar.astro').default; + export const SkipLink: typeof import('./components/SkipLink.astro').default; + export const ThemeProvider: typeof import('./components/ThemeProvider.astro').default; + + export const PageFrame: typeof import('./components/PageFrame.astro').default; + export const MobileMenuToggle: typeof import('./components/MobileMenuToggle.astro').default; + export const MobileMenuFooter: typeof import('./components/MobileMenuFooter.astro').default; + + export const TwoColumnContent: typeof import('./components/TwoColumnContent.astro').default; +} diff --git a/packages/starlight/vitest.config.ts b/packages/starlight/vitest.config.ts index ab9b2f4f41..fe6acc49c7 100644 --- a/packages/starlight/vitest.config.ts +++ b/packages/starlight/vitest.config.ts @@ -20,7 +20,18 @@ export default defineConfig({ coverage: { all: true, reportsDirectory: './__coverage__', - exclude: [...defaultCoverageExcludes, '**/vitest.*', 'components.ts', 'types.ts'], + exclude: [ + ...defaultCoverageExcludes, + '**/vitest.*', + 'components.ts', + 'types.ts', + // We use this to set up test environments so it isn‘t picked up, but we are testing it downstream. + 'integrations/virtual-user-config.ts', + // Types-only export. + 'props.ts', + // Main integration entrypoint — don’t think we’re able to test this directly currently. + 'index.ts', + ], thresholdAutoUpdate: true, lines: 69.21, functions: 90.24,