@astrojs/starlight 0.15.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1389](https://github.com/withastro/starlight/pull/1389) [`21b3620`](https://github.com/withastro/starlight/commit/21b36201aa1e01c8395d0f24b2fa4e32b90550bb) Thanks [@connor-baer](https://github.com/connor-baer)! - Adds new `disable404Route` config option to disable injection of Astro’s default 404 route
8
+
9
+ - [#1395](https://github.com/withastro/starlight/pull/1395) [`ce05dfb`](https://github.com/withastro/starlight/commit/ce05dfb4b1e9b90fad057d5d4328e4445f986b3b) Thanks [@hippotastic](https://github.com/hippotastic)! - Adds a new [`<Code>` component](https://starlight.astro.build/guides/components/#code) to render dynamic code strings with Expressive Code
10
+
11
+ ## 0.16.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#1383](https://github.com/withastro/starlight/pull/1383) [`490c6ef`](https://github.com/withastro/starlight/commit/490c6eff34ab408c4f55777b7b0caa16787dd3d4) Thanks [@delucis](https://github.com/delucis)! - Refactors Starlight’s internal virtual module system for components to avoid circular references
16
+
17
+ This is a change to an internal API.
18
+ If you were importing the internal `virtual:starlight/components` module, this no longer exists.
19
+ Update your imports to use the individual virtual modules now available for each component, for example `virtual:starlight/components/EditLink`.
20
+
21
+ - [#1151](https://github.com/withastro/starlight/pull/1151) [`134292d`](https://github.com/withastro/starlight/commit/134292ddd89683007d7de25545d39738a82c626c) Thanks [@kevinzunigacuellar](https://github.com/kevinzunigacuellar)! - Fixes sidebar auto-generation issue when a file and a directory, located at the same level, have identical names.
22
+
23
+ For example, `src/content/docs/guides.md` and `src/content/docs/guides/example.md` will now both be included and `src/content/docs/guides.md` is treated in the same way a `src/content/docs/guides/index.md` file would be.
24
+
25
+ - [#1386](https://github.com/withastro/starlight/pull/1386) [`0163634`](https://github.com/withastro/starlight/commit/0163634abb8578ce7a3d7ceea36432e98ea70e78) Thanks [@delucis](https://github.com/delucis)! - Tightens `line-height` on `<LinkCard>` titles to fix regression from original design
26
+
27
+ If you want to preserve the previous `line-height`, you can add the following custom CSS to your site:
28
+
29
+ ```css
30
+ .sl-link-card a {
31
+ line-height: 1.6;
32
+ }
33
+ ```
34
+
35
+ - [#1376](https://github.com/withastro/starlight/pull/1376) [`8398432`](https://github.com/withastro/starlight/commit/8398432aa4a0f38e2dd4452dfcdf7033c5713334) Thanks [@delucis](https://github.com/delucis)! - Tweaks vertical spacing in Markdown content styles.
36
+
37
+ This is a subtle change to Starlight’s default content styling that should improve most sites:
38
+
39
+ - Default vertical spacing between content items is reduced from `1.5rem` to `1rem`.
40
+ - Spacing before headings is now relative to font size, meaning higher-level headings have slightly more spacing and lower-level headings slightly less.
41
+
42
+ The overall impact is to tighten up content that belongs together and improve the visual hierarchy of headings to break up sections.
43
+
44
+ Although this is a subtle change, we recommend visually inspecting your site in case this impacts layout of any custom CSS or components.
45
+
46
+ If you want to preserve the previous spacing, you can add the following custom CSS to your site:
47
+
48
+ ```css
49
+ /* Restore vertical spacing to match Starlight v0.15 and below. */
50
+ .sl-markdown-content
51
+ :not(a, strong, em, del, span, input, code)
52
+ + :not(a, strong, em, del, span, input, code, :where(.not-content *)) {
53
+ margin-top: 1.5rem;
54
+ }
55
+ .sl-markdown-content
56
+ :not(h1, h2, h3, h4, h5, h6)
57
+ + :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
58
+ margin-top: 2.5rem;
59
+ }
60
+ ```
61
+
62
+ - [#1372](https://github.com/withastro/starlight/pull/1372) [`773880d`](https://github.com/withastro/starlight/commit/773880de87b79bf3107dbc32df29a86dd11e4e6f) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Updates the table of contents highlighting styles to prevent UI shifts when scrolling through a page.
63
+
64
+ If you want to preserve the previous, buggy styling, you can add the following custom CSS to your site:
65
+
66
+ ```css
67
+ starlight-toc a[aria-current='true'],
68
+ starlight-toc a[aria-current='true']:hover,
69
+ starlight-toc a[aria-current='true']:focus {
70
+ font-weight: 600;
71
+ color: var(--sl-color-text-invert);
72
+ background-color: var(--sl-color-text-accent);
73
+ }
74
+ ```
75
+
3
76
  ## 0.15.4
4
77
 
5
78
  ### Patch Changes
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
3
 
4
- import { EditLink, LastUpdated, Pagination } from 'virtual:starlight/components';
4
+ import EditLink from 'virtual:starlight/components/EditLink';
5
+ import LastUpdated from 'virtual:starlight/components/LastUpdated';
6
+ import Pagination from 'virtual:starlight/components/Pagination';
5
7
  ---
6
8
 
7
9
  <footer>
@@ -2,13 +2,11 @@
2
2
  import config from 'virtual:starlight/user-config';
3
3
  import type { Props } from '../props';
4
4
 
5
- import {
6
- LanguageSelect,
7
- Search,
8
- SiteTitle,
9
- SocialIcons,
10
- ThemeSelect,
11
- } from 'virtual:starlight/components';
5
+ import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
6
+ import Search from 'virtual:starlight/components/Search';
7
+ import SiteTitle from 'virtual:starlight/components/SiteTitle';
8
+ import SocialIcons from 'virtual:starlight/components/SocialIcons';
9
+ import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
12
10
 
13
11
  /**
14
12
  * Render the `Search` component if Pagefind is enabled or the default search component has been overridden.
@@ -1,5 +1,7 @@
1
1
  ---
2
- import { LanguageSelect, ThemeSelect, SocialIcons } from 'virtual:starlight/components';
2
+ import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
3
+ import SocialIcons from 'virtual:starlight/components/SocialIcons';
4
+ import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
3
5
  import type { Props } from '../props';
4
6
  ---
5
7
 
@@ -8,23 +8,21 @@ import '../style/shiki.css';
8
8
  import '../style/util.css';
9
9
 
10
10
  // Components — can override built-in CSS, but not user CSS.
11
- import {
12
- Banner,
13
- ContentPanel,
14
- PageTitle,
15
- FallbackContentNotice,
16
- Footer,
17
- Header,
18
- Head,
19
- Hero,
20
- MarkdownContent,
21
- PageSidebar,
22
- Sidebar,
23
- SkipLink,
24
- ThemeProvider,
25
- PageFrame,
26
- TwoColumnContent,
27
- } from 'virtual:starlight/components';
11
+ import Banner from 'virtual:starlight/components/Banner';
12
+ import ContentPanel from 'virtual:starlight/components/ContentPanel';
13
+ import FallbackContentNotice from 'virtual:starlight/components/FallbackContentNotice';
14
+ import Footer from 'virtual:starlight/components/Footer';
15
+ import Head from 'virtual:starlight/components/Head';
16
+ import Header from 'virtual:starlight/components/Header';
17
+ import Hero from 'virtual:starlight/components/Hero';
18
+ import MarkdownContent from 'virtual:starlight/components/MarkdownContent';
19
+ import PageFrame from 'virtual:starlight/components/PageFrame';
20
+ import PageSidebar from 'virtual:starlight/components/PageSidebar';
21
+ import PageTitle from 'virtual:starlight/components/PageTitle';
22
+ import Sidebar from 'virtual:starlight/components/Sidebar';
23
+ import SkipLink from 'virtual:starlight/components/SkipLink';
24
+ import ThemeProvider from 'virtual:starlight/components/ThemeProvider';
25
+ import TwoColumnContent from 'virtual:starlight/components/TwoColumnContent';
28
26
 
29
27
  // Remark component CSS (needs to override `MarkdownContent.astro`)
30
28
  import '../style/asides.css';
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { MobileMenuToggle } from 'virtual:starlight/components';
2
+ import MobileMenuToggle from 'virtual:starlight/components/MobileMenuToggle';
3
3
  import type { Props } from '../props';
4
4
 
5
5
  const { hasSidebar, labels } = Astro.props;
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
3
 
4
- import { TableOfContents, MobileTableOfContents } from 'virtual:starlight/components';
4
+ import MobileTableOfContents from 'virtual:starlight/components/MobileTableOfContents';
5
+ import TableOfContents from 'virtual:starlight/components/TableOfContents';
5
6
  ---
6
7
 
7
8
  {
@@ -33,14 +34,14 @@ import { TableOfContents, MobileTableOfContents } from 'virtual:starlight/compon
33
34
  line-height: var(--sl-line-height-headings);
34
35
  margin-bottom: 0.5rem;
35
36
  }
36
- .right-sidebar-panel :global(a) {
37
+ .right-sidebar-panel :global(:where(a)) {
37
38
  display: block;
38
39
  font-size: var(--sl-text-xs);
39
40
  text-decoration: none;
40
41
  color: var(--sl-color-gray-3);
41
42
  overflow-wrap: anywhere;
42
43
  }
43
- .right-sidebar-panel :global(a:hover) {
44
+ .right-sidebar-panel :global(:where(a):hover) {
44
45
  color: var(--sl-color-white);
45
46
  }
46
47
  @media (min-width: 72rem) {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import type { Props } from '../props';
3
3
 
4
- import { MobileMenuFooter } from 'virtual:starlight/components';
4
+ import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter';
5
5
  import SidebarSublist from './SidebarSublist.astro';
6
6
 
7
7
  const { sidebar } = Astro.props;
@@ -38,12 +38,8 @@ const { toc, isMobile = false, depth = 0 } = Astro.props;
38
38
  padding-inline: calc(1rem * var(--depth) + var(--pad-inline)) var(--pad-inline);
39
39
  line-height: 1.25;
40
40
  }
41
- a[aria-current='true'],
42
- a[aria-current='true']:hover,
43
- a[aria-current='true']:focus {
44
- font-weight: 600;
45
- color: var(--sl-color-text-invert);
46
- background-color: var(--sl-color-text-accent);
41
+ a[aria-current='true'] {
42
+ color: var(--sl-color-text-accent);
47
43
  }
48
44
  .isMobile a {
49
45
  --pad-inline: 1rem;
package/components.ts CHANGED
@@ -4,3 +4,4 @@ export { default as Icon } from './user-components/Icon.astro';
4
4
  export { default as Tabs } from './user-components/Tabs.astro';
5
5
  export { default as TabItem } from './user-components/TabItem.astro';
6
6
  export { default as LinkCard } from './user-components/LinkCard.astro';
7
+ export { Code } from 'astro-expressive-code/components';
package/index.ts CHANGED
@@ -39,10 +39,12 @@ export default function StarlightIntegration({
39
39
 
40
40
  const useTranslations = createTranslationSystemFromFs(starlightConfig, config);
41
41
 
42
- injectRoute({
43
- pattern: '404',
44
- entrypoint: '@astrojs/starlight/404.astro',
45
- });
42
+ if (!userConfig.disable404Route) {
43
+ injectRoute({
44
+ pattern: '404',
45
+ entrypoint: '@astrojs/starlight/404.astro',
46
+ });
47
+ }
46
48
  injectRoute({
47
49
  pattern: '[...slug]',
48
50
  entrypoint: '@astrojs/starlight/index.astro',
@@ -51,9 +53,7 @@ export default function StarlightIntegration({
51
53
  // config or by a plugin.
52
54
  const allIntegrations = [...config.integrations, ...integrations];
53
55
  if (!allIntegrations.find(({ name }) => name === 'astro-expressive-code')) {
54
- integrations.push(
55
- ...starlightExpressiveCode({ starlightConfig, astroConfig: config, useTranslations })
56
- );
56
+ integrations.push(...starlightExpressiveCode({ starlightConfig, useTranslations }));
57
57
  }
58
58
  if (!allIntegrations.find(({ name }) => name === '@astrojs/sitemap')) {
59
59
  integrations.push(starlightSitemap(starlightConfig));
@@ -34,3 +34,5 @@
34
34
  */
35
35
 
36
36
  export * from 'astro-expressive-code';
37
+
38
+ export { getStarlightEcConfigPreprocessor } from './index';
@@ -2,8 +2,9 @@ import {
2
2
  astroExpressiveCode,
3
3
  type AstroExpressiveCodeOptions,
4
4
  addClassName,
5
+ type CustomConfigPreprocessors,
5
6
  } from 'astro-expressive-code';
6
- import type { AstroConfig, AstroIntegration } from 'astro';
7
+ import type { AstroIntegration } from 'astro';
7
8
  import type { StarlightConfig } from '../../types';
8
9
  import type { createTranslationSystemFromFs } from '../../utils/translations-fs';
9
10
  import { pathToLocale } from '../shared/pathToLocale';
@@ -60,56 +61,59 @@ export type StarlightExpressiveCodeOptions = Omit<AstroExpressiveCodeOptions, 't
60
61
  useStarlightUiThemeColors?: boolean | undefined;
61
62
  };
62
63
 
63
- export const starlightExpressiveCode = ({
64
- astroConfig,
64
+ type StarlightEcIntegrationOptions = {
65
+ starlightConfig: StarlightConfig;
66
+ useTranslations?: ReturnType<typeof createTranslationSystemFromFs> | undefined;
67
+ };
68
+
69
+ /**
70
+ * Create an Expressive Code configuration preprocessor based on Starlight config.
71
+ * Used internally to set up Expressive Code and by the `<Code>` component.
72
+ */
73
+ export function getStarlightEcConfigPreprocessor({
65
74
  starlightConfig,
66
75
  useTranslations,
67
- }: {
68
- astroConfig: Pick<AstroConfig, 'root' | 'srcDir'>;
69
- starlightConfig: StarlightConfig;
70
- useTranslations: ReturnType<typeof createTranslationSystemFromFs>;
71
- }): AstroIntegration[] => {
72
- const { locales, expressiveCode } = starlightConfig;
73
- if (expressiveCode === false) return [];
74
- const config: StarlightExpressiveCodeOptions =
75
- typeof expressiveCode === 'object' ? expressiveCode : {};
76
+ }: StarlightEcIntegrationOptions): CustomConfigPreprocessors['preprocessAstroIntegrationConfig'] {
77
+ return (input): AstroExpressiveCodeOptions => {
78
+ const astroConfig = input.astroConfig;
79
+ const ecConfig = input.ecConfig as StarlightExpressiveCodeOptions;
80
+ const { locales } = starlightConfig;
76
81
 
77
- const {
78
- themes: themesInput,
79
- customizeTheme,
80
- styleOverrides: { textMarkers: textMarkersStyleOverrides, ...otherStyleOverrides } = {},
81
- useStarlightDarkModeSwitch,
82
- useStarlightUiThemeColors = config.themes === undefined,
83
- plugins = [],
84
- ...rest
85
- } = config;
82
+ const {
83
+ themes: themesInput,
84
+ customizeTheme,
85
+ styleOverrides: { textMarkers: textMarkersStyleOverrides, ...otherStyleOverrides } = {},
86
+ useStarlightDarkModeSwitch,
87
+ useStarlightUiThemeColors = ecConfig.themes === undefined,
88
+ plugins = [],
89
+ ...rest
90
+ } = ecConfig;
86
91
 
87
- // Handle the `themes` option
88
- const themes = preprocessThemes(themesInput);
89
- if (useStarlightUiThemeColors === true && themes.length < 2) {
90
- console.warn(
91
- `*** Warning: Using the config option "useStarlightUiThemeColors: true" ` +
92
- `with a single theme is not recommended. For better color contrast, ` +
93
- `please provide at least one dark and one light theme.\n`
94
- );
95
- }
92
+ // Handle the `themes` option
93
+ const themes = preprocessThemes(themesInput);
94
+ if (useStarlightUiThemeColors === true && themes.length < 2) {
95
+ console.warn(
96
+ `*** Warning: Using the config option "useStarlightUiThemeColors: true" ` +
97
+ `with a single theme is not recommended. For better color contrast, ` +
98
+ `please provide at least one dark and one light theme.\n`
99
+ );
100
+ }
96
101
 
97
- // Add the `not-content` class to all rendered blocks to prevent them from being affected
98
- // by Starlight's default content styles
99
- plugins.push({
100
- name: 'Starlight Plugin',
101
- hooks: {
102
- postprocessRenderedBlock: ({ renderData }) => {
103
- addClassName(renderData.blockAst, 'not-content');
102
+ // Add the `not-content` class to all rendered blocks to prevent them from being affected
103
+ // by Starlight's default content styles
104
+ plugins.push({
105
+ name: 'Starlight Plugin',
106
+ hooks: {
107
+ postprocessRenderedBlock: ({ renderData }) => {
108
+ addClassName(renderData.blockAst, 'not-content');
109
+ },
104
110
  },
105
- },
106
- });
111
+ });
107
112
 
108
- // Add Expressive Code UI translations (if any) for all defined locales
109
- addTranslations(locales, useTranslations);
113
+ // Add Expressive Code UI translations (if any) for all defined locales
114
+ if (useTranslations) addTranslations(locales, useTranslations);
110
115
 
111
- return [
112
- astroExpressiveCode({
116
+ return {
113
117
  themes,
114
118
  customizeTheme: (theme) => {
115
119
  if (useStarlightUiThemeColors) {
@@ -151,6 +155,36 @@ export const starlightExpressiveCode = ({
151
155
  getBlockLocale: ({ file }) => pathToLocale(file.path, { starlightConfig, astroConfig }),
152
156
  plugins,
153
157
  ...rest,
158
+ };
159
+ };
160
+ }
161
+
162
+ export const starlightExpressiveCode = ({
163
+ starlightConfig,
164
+ useTranslations,
165
+ }: StarlightEcIntegrationOptions): AstroIntegration[] => {
166
+ if (starlightConfig.expressiveCode === false) return [];
167
+
168
+ const configArgs =
169
+ typeof starlightConfig.expressiveCode === 'object'
170
+ ? (starlightConfig.expressiveCode as AstroExpressiveCodeOptions)
171
+ : {};
172
+ return [
173
+ astroExpressiveCode({
174
+ ...configArgs,
175
+ customConfigPreprocessors: {
176
+ preprocessAstroIntegrationConfig: getStarlightEcConfigPreprocessor({
177
+ starlightConfig,
178
+ useTranslations,
179
+ }),
180
+ preprocessComponentConfig: `
181
+ import starlightConfig from 'virtual:starlight/user-config'
182
+ import { useTranslations } from '@astrojs/starlight/internal'
183
+ import { getStarlightEcConfigPreprocessor } from '@astrojs/starlight/expressive-code'
184
+
185
+ export default getStarlightEcConfigPreprocessor({ starlightConfig, useTranslations })
186
+ `,
187
+ },
154
188
  }),
155
189
  ];
156
190
  };
@@ -1,5 +1,6 @@
1
- import fs from 'node:fs';
2
1
  import { ExpressiveCodeTheme, type ThemeObjectOrShikiThemeName } from 'astro-expressive-code';
2
+ import nightOwlDark from './themes/night-owl-dark.jsonc?raw';
3
+ import nightOwlLight from './themes/night-owl-light.jsonc?raw';
3
4
 
4
5
  export type BundledThemeName = 'starlight-dark' | 'starlight-light';
5
6
 
@@ -20,13 +21,8 @@ export function preprocessThemes(
20
21
  return themes.map((theme) => {
21
22
  // If the current entry is the name of a bundled theme, load it
22
23
  if (theme === 'starlight-dark' || theme === 'starlight-light') {
23
- const bundledThemeFile =
24
- theme === 'starlight-dark' ? 'night-owl-dark.jsonc' : 'night-owl-light.jsonc';
25
- return customizeBundledTheme(
26
- ExpressiveCodeTheme.fromJSONString(
27
- fs.readFileSync(new URL(`./themes/${bundledThemeFile}`, import.meta.url), 'utf-8')
28
- )
29
- );
24
+ const bundledTheme = theme === 'starlight-dark' ? nightOwlDark : nightOwlLight;
25
+ return customizeBundledTheme(ExpressiveCodeTheme.fromJSONString(bundledTheme));
30
26
  }
31
27
  // Otherwise, just pass it through
32
28
  return theme;
@@ -22,11 +22,13 @@ export function pathToLocale(
22
22
  ): string | undefined {
23
23
  const srcDir = new URL(astroConfig.srcDir, astroConfig.root);
24
24
  const docsDir = new URL('content/docs/', srcDir);
25
- const slug = path
26
- // Format path to unix style path.
27
- ?.replace(/\\/g, '/')
28
- // Strip docs path leaving only content collection file ID.
29
- // Example: /Users/houston/repo/src/content/docs/en/guide.md => en/guide.md
30
- .replace(docsDir.pathname, '');
25
+ // Format path to unix style path.
26
+ path = path?.replace(/\\/g, '/');
27
+ // Ensure that the page path starts with a slash if the docs directory also does,
28
+ // which makes stripping the docs path in the next step work on Windows, too.
29
+ if (path && !path.startsWith('/') && docsDir.pathname.startsWith('/')) path = '/' + path;
30
+ // Strip docs path leaving only content collection file ID.
31
+ // Example: /Users/houston/repo/src/content/docs/en/guide.md => en/guide.md
32
+ const slug = path?.replace(docsDir.pathname, '');
31
33
  return slugToLocale(slug, starlightConfig.locales);
32
34
  }
@@ -22,6 +22,13 @@ export function vitePluginStarlightUserConfig(
22
22
  const resolveId = (id: string) =>
23
23
  JSON.stringify(id.startsWith('.') ? resolve(fileURLToPath(root), id) : id);
24
24
 
25
+ const virtualComponentModules = Object.fromEntries(
26
+ Object.entries(opts.components).map(([name, path]) => [
27
+ `virtual:starlight/components/${name}`,
28
+ `export { default } from ${resolveId(path)};`,
29
+ ])
30
+ );
31
+
25
32
  /** Map of virtual module names to their code contents as strings. */
26
33
  const modules = {
27
34
  'virtual:starlight/user-config': `export default ${JSON.stringify(opts)}`,
@@ -41,9 +48,7 @@ export function vitePluginStarlightUserConfig(
41
48
  opts.logo.light
42
49
  )}; export const logos = { dark, light };`
43
50
  : 'export const logos = {};',
44
- 'virtual:starlight/components': Object.entries(opts.components)
45
- .map(([name, path]) => `export { default as ${name} } from ${resolveId(path)};`)
46
- .join(''),
51
+ ...virtualComponentModules,
47
52
  } satisfies Record<string, string>;
48
53
 
49
54
  /** Mapping names prefixed with `\0` to their original form. */
package/internal.ts ADDED
@@ -0,0 +1 @@
1
+ export { useTranslations } from './utils/translations';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.15.4",
3
+ "version": "0.17.0",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -150,6 +150,7 @@
150
150
  "types": "./components/Search.astro.tsx",
151
151
  "import": "./components/Search.astro"
152
152
  },
153
+ "./internal": "./internal.ts",
153
154
  "./props": "./props.ts",
154
155
  "./schema": "./schema.ts",
155
156
  "./types": "./types.ts",
@@ -165,7 +166,7 @@
165
166
  "@astrojs/markdown-remark": "^4.0.1",
166
167
  "@types/node": "^18.16.19",
167
168
  "@vitest/coverage-v8": "^0.33.0",
168
- "astro": "^4.0.1",
169
+ "astro": "^4.2.1",
169
170
  "vitest": "^0.33.0"
170
171
  },
171
172
  "dependencies": {
@@ -174,7 +175,7 @@
174
175
  "@pagefind/default-ui": "^1.0.3",
175
176
  "@types/hast": "^3.0.3",
176
177
  "@types/mdast": "^4.0.3",
177
- "astro-expressive-code": "^0.31.0",
178
+ "astro-expressive-code": "^0.32.2",
178
179
  "bcp-47": "^2.1.0",
179
180
  "hast-util-select": "^6.0.2",
180
181
  "hastscript": "^8.0.0",
@@ -1,14 +1,14 @@
1
1
  .sl-markdown-content
2
2
  :not(a, strong, em, del, span, input, code)
3
3
  + :not(a, strong, em, del, span, input, code, :where(.not-content *)) {
4
- margin-top: 1.5rem;
4
+ margin-top: 1rem;
5
5
  }
6
6
 
7
7
  /* Headings after non-headings have more spacing. */
8
8
  .sl-markdown-content
9
9
  :not(h1, h2, h3, h4, h5, h6)
10
10
  + :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
11
- margin-top: 2.5rem;
11
+ margin-top: 1.5em;
12
12
  }
13
13
 
14
14
  .sl-markdown-content li + li:not(:where(.not-content *)),
@@ -10,7 +10,7 @@ interface Props extends Omit<HTMLAttributes<'a'>, 'title'> {
10
10
  const { title, description, ...attributes } = Astro.props;
11
11
  ---
12
12
 
13
- <div>
13
+ <div class="sl-link-card">
14
14
  <span class="sl-flex stack">
15
15
  <a {...attributes}>
16
16
  <span class="title" set:html={title} />
@@ -21,7 +21,7 @@ const { title, description, ...attributes } = Astro.props;
21
21
  </div>
22
22
 
23
23
  <style>
24
- div {
24
+ .sl-link-card {
25
25
  display: grid;
26
26
  grid-template-columns: 1fr auto;
27
27
  gap: 0.5rem;
@@ -34,6 +34,7 @@ const { title, description, ...attributes } = Astro.props;
34
34
 
35
35
  a {
36
36
  text-decoration: none;
37
+ line-height: var(--sl-line-height-headings);
37
38
  }
38
39
 
39
40
  /* a11y fix for https://github.com/withastro/starlight/issues/487 */
@@ -52,7 +53,6 @@ const { title, description, ...attributes } = Astro.props;
52
53
  color: var(--sl-color-white);
53
54
  font-weight: 600;
54
55
  font-size: var(--sl-text-lg);
55
- line-height: var(--sl-line-height-headings);
56
56
  }
57
57
 
58
58
  .description {
@@ -65,12 +65,12 @@ const { title, description, ...attributes } = Astro.props;
65
65
  }
66
66
 
67
67
  /* Hover state */
68
- div:hover {
68
+ .sl-link-card:hover {
69
69
  background: var(--sl-color-gray-7, var(--sl-color-gray-6));
70
70
  border-color: var(--sl-color-gray-2);
71
71
  }
72
72
 
73
- div:hover .icon {
73
+ .sl-link-card:hover .icon {
74
74
  color: var(--sl-color-white);
75
75
  }
76
76
  </style>
@@ -1,4 +1,3 @@
1
- import { basename, dirname } from 'node:path';
2
1
  import config from 'virtual:starlight/user-config';
3
2
  import type { Badge } from '../schemas/badge';
4
3
  import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
@@ -11,11 +10,12 @@ import type {
11
10
  import { createPathFormatter } from './createPathFormatter';
12
11
  import { formatPath } from './format-path';
13
12
  import { pickLang } from './i18n';
14
- import { ensureLeadingSlash } from './path';
13
+ import { ensureLeadingSlash, ensureTrailingSlash, stripLeadingAndTrailingSlashes } from './path';
15
14
  import { getLocaleRoutes, type Route } from './routing';
16
15
  import { localeToLang, slugToPathname } from './slugs';
17
16
 
18
17
  const DirKey = Symbol('DirKey');
18
+ const SlugKey = Symbol('SlugKey');
19
19
 
20
20
  export interface Link {
21
21
  type: 'link';
@@ -44,14 +44,16 @@ export type SidebarEntry = Link | Group;
44
44
  */
45
45
  interface Dir {
46
46
  [DirKey]: undefined;
47
+ [SlugKey]: string;
47
48
  [item: string]: Dir | Route;
48
49
  }
49
50
 
50
51
  /** Create a new directory object. */
51
- function makeDir(): Dir {
52
+ function makeDir(slug: string): Dir {
52
53
  const dir = {} as Dir;
53
- // Add DirKey as a non-enumerable property so that `Object.entries(dir)` ignores it.
54
+ // Add DirKey and SlugKey as non-enumerable properties so that `Object.entries(dir)` ignores them.
54
55
  Object.defineProperty(dir, DirKey, { enumerable: false });
56
+ Object.defineProperty(dir, SlugKey, { value: slug, enumerable: false });
55
57
  return dir;
56
58
  }
57
59
 
@@ -157,37 +159,48 @@ function getBreadcrumbs(path: string, baseDir: string): string[] {
157
159
  // Index paths will match `baseDir` and don’t include breadcrumbs.
158
160
  if (pathWithoutExt === baseDir) return [];
159
161
  // Ensure base directory ends in a trailing slash.
160
- if (!baseDir.endsWith('/')) baseDir += '/';
162
+ baseDir = ensureTrailingSlash(baseDir);
161
163
  // Strip base directory from path if present.
162
164
  const relativePath = pathWithoutExt.startsWith(baseDir)
163
165
  ? pathWithoutExt.replace(baseDir, '')
164
166
  : pathWithoutExt;
165
- let dir = dirname(relativePath);
166
- // Return no breadcrumbs for items in the root directory.
167
- if (dir === '.') return [];
168
- return dir.split('/');
167
+
168
+ return relativePath.split('/');
169
169
  }
170
170
 
171
171
  /** Turn a flat array of routes into a tree structure. */
172
172
  function treeify(routes: Route[], baseDir: string): Dir {
173
- const treeRoot: Dir = makeDir();
173
+ const treeRoot: Dir = makeDir(baseDir);
174
174
  routes
175
175
  // Remove any entries that should be hidden
176
176
  .filter((doc) => !doc.entry.data.sidebar.hidden)
177
+ // Sort by depth, to build the tree depth first.
178
+ .sort((a, b) => b.id.split('/').length - a.id.split('/').length)
179
+ // Build the tree
177
180
  .forEach((doc) => {
178
- const breadcrumbs = getBreadcrumbs(doc.id, baseDir);
179
-
180
- // Walk down the route’s path to generate the tree.
181
- let currentDir = treeRoot;
182
- breadcrumbs.forEach((dir) => {
183
- // Create new folder if needed.
184
- if (typeof currentDir[dir] === 'undefined') currentDir[dir] = makeDir();
185
- // Go into the subdirectory.
186
- currentDir = currentDir[dir] as Dir;
181
+ const parts = getBreadcrumbs(doc.id, baseDir);
182
+ let currentNode = treeRoot;
183
+
184
+ parts.forEach((part, index) => {
185
+ const isLeaf = index === parts.length - 1;
186
+
187
+ // Handle directory index pages by renaming them to `index`
188
+ if (isLeaf && currentNode.hasOwnProperty(part)) {
189
+ currentNode = currentNode[part] as Dir;
190
+ part = 'index';
191
+ }
192
+
193
+ // Recurse down the tree if this isn’t the leaf node.
194
+ if (!isLeaf) {
195
+ const path = currentNode[SlugKey];
196
+ currentNode[part] ||= makeDir(stripLeadingAndTrailingSlashes(path + '/' + part));
197
+ currentNode = currentNode[part] as Dir;
198
+ } else {
199
+ currentNode[part] = doc;
200
+ }
187
201
  });
188
- // We’ve walked through the path. Register the route in this directory.
189
- currentDir[basename(doc.slug)] = doc;
190
202
  });
203
+
191
204
  return treeRoot;
192
205
  }
193
206
 
@@ -212,24 +225,16 @@ function getOrder(routeOrDir: Route | Dir): number {
212
225
  : // If no order value is found, set it to the largest number possible.
213
226
  routeOrDir.entry.data.sidebar.order ?? Number.MAX_VALUE;
214
227
  }
215
- /** Get the comparison ID for a given route to sort them alphabetically. */
216
- function getComparisonId(id: string) {
217
- const filename = stripExtension(basename(id));
218
- return filename === 'index' ? '' : filename;
219
- }
220
228
 
221
229
  /** Sort a directory’s entries by user-specified order or alphabetically if no order specified. */
222
230
  function sortDirEntries(dir: [string, Dir | Route][]): [string, Dir | Route][] {
223
231
  const collator = new Intl.Collator(localeToLang(undefined));
224
- return dir.sort(([keyA, a], [keyB, b]) => {
232
+ return dir.sort(([_keyA, a], [_keyB, b]) => {
225
233
  const [aOrder, bOrder] = [getOrder(a), getOrder(b)];
226
234
  // Pages are sorted by order in ascending order.
227
235
  if (aOrder !== bOrder) return aOrder < bOrder ? -1 : 1;
228
236
  // If two pages have the same order value they will be sorted by their slug.
229
- return collator.compare(
230
- isDir(a) ? keyA : getComparisonId(a.id),
231
- isDir(b) ? keyB : getComparisonId(b.id)
232
- );
237
+ return collator.compare(isDir(a) ? a[SlugKey] : a.slug, isDir(b) ? b[SlugKey] : b.slug);
233
238
  });
234
239
  }
235
240
 
@@ -205,6 +205,9 @@ const UserConfigSchema = z.object({
205
205
  .string()
206
206
  .default('|')
207
207
  .describe('Will be used as title delimiter in the generated `<title>` tag.'),
208
+
209
+ /** Disable Starlight's default 404 page. */
210
+ disable404Route: z.boolean().default(false).describe("Disable Starlight's default 404 page."),
208
211
  });
209
212
 
210
213
  export const StarlightConfigSchema = UserConfigSchema.strict().transform(
package/virtual.d.ts CHANGED
@@ -24,39 +24,118 @@ declare module 'virtual:starlight/user-images' {
24
24
  };
25
25
  }
26
26
 
27
- declare module 'virtual:starlight/components' {
28
- export const Banner: typeof import('./components/Banner.astro').default;
29
- export const ContentPanel: typeof import('./components/ContentPanel.astro').default;
30
- export const PageTitle: typeof import('./components/PageTitle.astro').default;
31
- export const FallbackContentNotice: typeof import('./components/FallbackContentNotice.astro').default;
32
-
33
- export const Footer: typeof import('./components/Footer.astro').default;
34
- export const LastUpdated: typeof import('./components/LastUpdated.astro').default;
35
- export const Pagination: typeof import('./components/Pagination.astro').default;
36
- export const EditLink: typeof import('./components/EditLink.astro').default;
37
-
38
- export const Header: typeof import('./components/Header.astro').default;
39
- export const LanguageSelect: typeof import('./components/LanguageSelect.astro').default;
40
- export const Search: typeof import('./components/Search.astro').default;
41
- export const SiteTitle: typeof import('./components/SiteTitle.astro').default;
42
- export const SocialIcons: typeof import('./components/SocialIcons.astro').default;
43
- export const ThemeSelect: typeof import('./components/ThemeSelect.astro').default;
44
-
45
- export const Head: typeof import('./components/Head.astro').default;
46
- export const Hero: typeof import('./components/Hero.astro').default;
47
- export const MarkdownContent: typeof import('./components/MarkdownContent.astro').default;
48
-
49
- export const PageSidebar: typeof import('./components/PageSidebar.astro').default;
50
- export const TableOfContents: typeof import('./components/TableOfContents.astro').default;
51
- export const MobileTableOfContents: typeof import('./components/MobileTableOfContents.astro').default;
52
-
53
- export const Sidebar: typeof import('./components/Sidebar.astro').default;
54
- export const SkipLink: typeof import('./components/SkipLink.astro').default;
55
- export const ThemeProvider: typeof import('./components/ThemeProvider.astro').default;
56
-
57
- export const PageFrame: typeof import('./components/PageFrame.astro').default;
58
- export const MobileMenuToggle: typeof import('./components/MobileMenuToggle.astro').default;
59
- export const MobileMenuFooter: typeof import('./components/MobileMenuFooter.astro').default;
60
-
61
- export const TwoColumnContent: typeof import('./components/TwoColumnContent.astro').default;
27
+ declare module 'virtual:starlight/components/Banner' {
28
+ const Banner: typeof import('./components/Banner.astro').default;
29
+ export default Banner;
30
+ }
31
+ declare module 'virtual:starlight/components/ContentPanel' {
32
+ const ContentPanel: typeof import('./components/ContentPanel.astro').default;
33
+ export default ContentPanel;
34
+ }
35
+ declare module 'virtual:starlight/components/PageTitle' {
36
+ const PageTitle: typeof import('./components/PageTitle.astro').default;
37
+ export default PageTitle;
38
+ }
39
+ declare module 'virtual:starlight/components/FallbackContentNotice' {
40
+ const FallbackContentNotice: typeof import('./components/FallbackContentNotice.astro').default;
41
+ export default FallbackContentNotice;
42
+ }
43
+
44
+ declare module 'virtual:starlight/components/Footer' {
45
+ const Footer: typeof import('./components/Footer.astro').default;
46
+ export default Footer;
47
+ }
48
+ declare module 'virtual:starlight/components/LastUpdated' {
49
+ const LastUpdated: typeof import('./components/LastUpdated.astro').default;
50
+ export default LastUpdated;
51
+ }
52
+ declare module 'virtual:starlight/components/Pagination' {
53
+ const Pagination: typeof import('./components/Pagination.astro').default;
54
+ export default Pagination;
55
+ }
56
+ declare module 'virtual:starlight/components/EditLink' {
57
+ const EditLink: typeof import('./components/EditLink.astro').default;
58
+ export default EditLink;
59
+ }
60
+
61
+ declare module 'virtual:starlight/components/Header' {
62
+ const Header: typeof import('./components/Header.astro').default;
63
+ export default Header;
64
+ }
65
+ declare module 'virtual:starlight/components/LanguageSelect' {
66
+ const LanguageSelect: typeof import('./components/LanguageSelect.astro').default;
67
+ export default LanguageSelect;
68
+ }
69
+ declare module 'virtual:starlight/components/Search' {
70
+ const Search: typeof import('./components/Search.astro').default;
71
+ export default Search;
72
+ }
73
+ declare module 'virtual:starlight/components/SiteTitle' {
74
+ const SiteTitle: typeof import('./components/SiteTitle.astro').default;
75
+ export default SiteTitle;
76
+ }
77
+ declare module 'virtual:starlight/components/SocialIcons' {
78
+ const SocialIcons: typeof import('./components/SocialIcons.astro').default;
79
+ export default SocialIcons;
80
+ }
81
+ declare module 'virtual:starlight/components/ThemeSelect' {
82
+ const ThemeSelect: typeof import('./components/ThemeSelect.astro').default;
83
+ export default ThemeSelect;
84
+ }
85
+
86
+ declare module 'virtual:starlight/components/Head' {
87
+ const Head: typeof import('./components/Head.astro').default;
88
+ export default Head;
89
+ }
90
+ declare module 'virtual:starlight/components/Hero' {
91
+ const Hero: typeof import('./components/Hero.astro').default;
92
+ export default Hero;
93
+ }
94
+ declare module 'virtual:starlight/components/MarkdownContent' {
95
+ const MarkdownContent: typeof import('./components/MarkdownContent.astro').default;
96
+ export default MarkdownContent;
97
+ }
98
+
99
+ declare module 'virtual:starlight/components/PageSidebar' {
100
+ const PageSidebar: typeof import('./components/PageSidebar.astro').default;
101
+ export default PageSidebar;
102
+ }
103
+ declare module 'virtual:starlight/components/TableOfContents' {
104
+ const TableOfContents: typeof import('./components/TableOfContents.astro').default;
105
+ export default TableOfContents;
106
+ }
107
+ declare module 'virtual:starlight/components/MobileTableOfContents' {
108
+ const MobileTableOfContents: typeof import('./components/MobileTableOfContents.astro').default;
109
+ export default MobileTableOfContents;
110
+ }
111
+
112
+ declare module 'virtual:starlight/components/Sidebar' {
113
+ const Sidebar: typeof import('./components/Sidebar.astro').default;
114
+ export default Sidebar;
115
+ }
116
+ declare module 'virtual:starlight/components/SkipLink' {
117
+ const SkipLink: typeof import('./components/SkipLink.astro').default;
118
+ export default SkipLink;
119
+ }
120
+ declare module 'virtual:starlight/components/ThemeProvider' {
121
+ const ThemeProvider: typeof import('./components/ThemeProvider.astro').default;
122
+ export default ThemeProvider;
123
+ }
124
+
125
+ declare module 'virtual:starlight/components/PageFrame' {
126
+ const PageFrame: typeof import('./components/PageFrame.astro').default;
127
+ export default PageFrame;
128
+ }
129
+ declare module 'virtual:starlight/components/MobileMenuToggle' {
130
+ const MobileMenuToggle: typeof import('./components/MobileMenuToggle.astro').default;
131
+ export default MobileMenuToggle;
132
+ }
133
+ declare module 'virtual:starlight/components/MobileMenuFooter' {
134
+ const MobileMenuFooter: typeof import('./components/MobileMenuFooter.astro').default;
135
+ export default MobileMenuFooter;
136
+ }
137
+
138
+ declare module 'virtual:starlight/components/TwoColumnContent' {
139
+ const TwoColumnContent: typeof import('./components/TwoColumnContent.astro').default;
140
+ export default TwoColumnContent;
62
141
  }