@astrojs/starlight 0.0.5 → 0.0.7

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/404.astro CHANGED
@@ -1,8 +1,5 @@
1
1
  ---
2
- import Header from './components/Header.astro';
3
- import MarkdownContent from './components/MarkdownContent.astro';
4
- import ThemeProvider from './components/ThemeProvider.astro';
5
- import PageFrame from './layout/PageFrame.astro';
2
+ import config from 'virtual:starlight/user-config';
6
3
 
7
4
  // Built-in CSS styles.
8
5
  import './style/props.css';
@@ -10,10 +7,18 @@ import './style/reset.css';
10
7
  import './style/shiki.css';
11
8
  import './style/util.css';
12
9
 
13
- // TODO: replace with proper values — requires support for a “default” locale
14
- const lang = 'en';
15
- const dir = 'ltr';
16
- const locale = undefined;
10
+ // Layout
11
+ import PageFrame from './layout/PageFrame.astro';
12
+
13
+ // Components
14
+ import Header from './components/Header.astro';
15
+ import MarkdownContent from './components/MarkdownContent.astro';
16
+ import ThemeProvider from './components/ThemeProvider.astro';
17
+
18
+ // Important that this is the last import so it can override built-in styles.
19
+ import 'virtual:starlight/user-css';
20
+
21
+ const { lang = 'en', dir = 'ltr', locale } = config.defaultLocale || {};
17
22
  ---
18
23
 
19
24
  <html lang={lang} dir={dir}>
@@ -26,9 +31,9 @@ const locale = undefined;
26
31
  <ThemeProvider />
27
32
  <PageFrame>
28
33
  <Header slot="header" locale={locale} />
29
- <main id="starlight__overview">
34
+ <main>
30
35
  <MarkdownContent>
31
- <h1>404</h1>
36
+ <h1 id="starlight__overview">404</h1>
32
37
  <p>Houston, we have a problem.</p>
33
38
  <p>
34
39
  We couldn’t find that link. Check the address or <a
@@ -40,7 +45,7 @@ const locale = undefined;
40
45
  </PageFrame>
41
46
 
42
47
  <style>
43
- #starlight__overview {
48
+ main {
44
49
  margin: auto;
45
50
  padding: clamp(2rem, 10vmin, 6rem) var(--sl-nav-pad-x);
46
51
  max-width: var(--sl-content-width);
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - [#55](https://github.com/withastro/starlight/pull/55) [`8597b9c`](https://github.com/withastro/starlight/commit/8597b9c1002f8c5073d25ae5cacd4060ded2f8c8) Thanks [@delucis](https://github.com/delucis)! - Fix routing logic to handle `index.md` slug differences between docs collection root and nested directories.
8
+
9
+ - [#54](https://github.com/withastro/starlight/pull/54) [`db728d6`](https://github.com/withastro/starlight/commit/db728d61afa5cea060c66f746a4cc4ab3e1c3bcd) Thanks [@TheOtterlord](https://github.com/TheOtterlord)! - Add padding to scroll preventing headings being obscured by nav
10
+
11
+ - [#51](https://github.com/withastro/starlight/pull/51) [`3adbdbb`](https://github.com/withastro/starlight/commit/3adbdbbb71a4b3648984fa1028fa116d0aff9a7d) Thanks [@delucis](https://github.com/delucis)! - Support displaying a custom logo in the nav bar.
12
+
13
+ - [#51](https://github.com/withastro/starlight/pull/51) [`3adbdbb`](https://github.com/withastro/starlight/commit/3adbdbbb71a4b3648984fa1028fa116d0aff9a7d) Thanks [@delucis](https://github.com/delucis)! - All Starlight projects now use Astro’s experimental optimized asset support.
14
+
15
+ ## 0.0.6
16
+
17
+ ### Patch Changes
18
+
19
+ - [#47](https://github.com/withastro/starlight/pull/47) [`e96d9a7`](https://github.com/withastro/starlight/commit/e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d) Thanks [@delucis](https://github.com/delucis)! - Fix CSS ordering issue caused by imports in 404 route.
20
+
21
+ - [#47](https://github.com/withastro/starlight/pull/47) [`e96d9a7`](https://github.com/withastro/starlight/commit/e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d) Thanks [@delucis](https://github.com/delucis)! - Highlight current page section in table of contents.
22
+
23
+ - [`1028119`](https://github.com/withastro/starlight/commit/10281196aba65075e4ac202dc0f23927c44403ee) Thanks [@delucis](https://github.com/delucis)! - Use default locale in `404.astro`.
24
+
25
+ - [`05f8fd4`](https://github.com/withastro/starlight/commit/05f8fd4c3114e4c25075b35086c5b3e7d0ff49d7) Thanks [@delucis](https://github.com/delucis)! - Include `initial-scale=1` in viewport meta tag.
26
+
27
+ - [#47](https://github.com/withastro/starlight/pull/47) [`e96d9a7`](https://github.com/withastro/starlight/commit/e96d9a7628c5c04fe34dbc65ddd6fabdc0667a6d) Thanks [@delucis](https://github.com/delucis)! - Fix usage of `aria-current` in navigation sidebar to use `page` value.
28
+
29
+ - [#48](https://github.com/withastro/starlight/pull/48) [`a49485d`](https://github.com/withastro/starlight/commit/a49485def3fe4f505e90bf934eedcb135b3d3f51) Thanks [@delucis](https://github.com/delucis)! - Improve right sidebar layout.
30
+
3
31
  ## 0.0.5
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -12,6 +12,9 @@ Starlight is a documentation website framework for [Astro][astro].
12
12
 
13
13
  ## Contributing
14
14
 
15
+ Join us as a Starlight contributor! These links will help you get started:
16
+
17
+ - [Contributor Manual][contributing]
15
18
  - [Code of Conduct][coc]
16
19
  - [Community Guide][community]
17
20
 
@@ -23,6 +26,7 @@ Copyright (c) 2023–present [Starlight contributors][contributors]
23
26
 
24
27
  [astro]: https://astro.build/
25
28
  [docs]: https://starlight.astro.build/
29
+ [contributing]: https://github.com/withastro/starlight/blob/main/CONTRIBUTING.md
26
30
  [coc]: https://github.com/withastro/.github/blob/main/CODE_OF_CONDUCT.md
27
31
  [community]: https://github.com/withastro/.github/blob/main/COMMUNITY_GUIDE.md
28
32
  [contributors]: https://github.com/withastro/starlight/graphs/contributors
@@ -20,7 +20,10 @@ const description = data.description || config.description;
20
20
 
21
21
  const headDefaults: z.input<ReturnType<typeof HeadConfigSchema>> = [
22
22
  { tag: 'meta', attrs: { charset: 'utf-8' } },
23
- { tag: 'meta', attrs: { name: 'viewport', content: 'width=device-width' } },
23
+ {
24
+ tag: 'meta',
25
+ attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1' },
26
+ },
24
27
  { tag: 'title', content: title },
25
28
  { tag: 'link', attrs: { rel: 'canonical', href: canonical?.href } },
26
29
  { tag: 'meta', attrs: { name: 'generator', content: Astro.generator } },
@@ -1,9 +1,9 @@
1
1
  ---
2
- import config from 'virtual:starlight/user-config';
3
2
  import LanguageSelect from './LanguageSelect.astro';
4
3
  import Search from './Search.astro';
5
- import ThemeSelect from './ThemeSelect.astro';
4
+ import SiteTitle from './SiteTitle.astro';
6
5
  import SocialIcons from './SocialIcons.astro';
6
+ import ThemeSelect from './ThemeSelect.astro';
7
7
 
8
8
  interface Props {
9
9
  locale: string | undefined;
@@ -11,7 +11,7 @@ interface Props {
11
11
  ---
12
12
 
13
13
  <div class="header">
14
- <a href={import.meta.env.BASE_URL} class="site-title">{config.title}</a>
14
+ <SiteTitle />
15
15
  <Search />
16
16
  <div class="hidden md:flex right-group">
17
17
  <SocialIcons />
@@ -29,14 +29,6 @@ interface Props {
29
29
  height: 100%;
30
30
  }
31
31
 
32
- .site-title {
33
- font-size: var(--sl-text-h4);
34
- font-weight: 600;
35
- color: var(--sl-color-text-accent);
36
- text-decoration: none;
37
- white-space: nowrap;
38
- }
39
-
40
32
  .right-group {
41
33
  gap: 1rem;
42
34
  align-items: center;
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  import type { MarkdownHeading } from 'astro';
3
- import type { CollectionEntry } from 'astro:content';
4
3
  import config from 'virtual:starlight/user-config';
4
+ import type { StarlightDocsEntry } from '../utils/routing';
5
5
  import EditLink from './EditLink.astro';
6
6
  import RightSidebarPanel from './RightSidebarPanel.astro';
7
7
  import TableOfContents from './TableOfContents.astro';
8
8
 
9
9
  interface Props {
10
- entry: CollectionEntry<'docs'>;
10
+ entry: StarlightDocsEntry;
11
11
  headings: MarkdownHeading[];
12
12
  }
13
13
 
@@ -12,18 +12,21 @@
12
12
  border-top: 1px solid var(--sl-color-hairline);
13
13
  }
14
14
  .container {
15
- width: var(--sl-sidebar-width);
15
+ width: calc(var(--sl-sidebar-width) - 2 * var(--sl-sidebar-pad-x));
16
16
  }
17
17
  .right-sidebar-panel :global(h2) {
18
18
  color: var(--sl-color-white);
19
- font-size: var(--sl-text-base);
19
+ font-size: var(--sl-text-h5);
20
20
  font-weight: 600;
21
+ line-height: var(--sl-line-height-headings);
22
+ margin-bottom: 0.5rem;
21
23
  }
22
24
  .right-sidebar-panel :global(a) {
23
25
  display: block;
24
26
  font-size: var(--sl-text-xs);
25
27
  text-decoration: none;
26
28
  color: var(--sl-color-gray-3);
29
+ word-break: break-word;
27
30
  }
28
31
  .right-sidebar-panel :global(a:hover) {
29
32
  color: var(--sl-color-white);
@@ -31,7 +34,12 @@
31
34
  @media (min-width: 72rem) {
32
35
  .container {
33
36
  max-width: calc(
34
- 100vw - var(--sl-content-width) - var(--sl-sidebar-width)
37
+ (
38
+ (
39
+ 100vw - var(--sl-sidebar-width) - 2 * var(--sl-content-pad-x) - 2 *
40
+ var(--sl-sidebar-pad-x)
41
+ ) * 0.25 /* MAGIC NUMBER 🥲 */
42
+ )
35
43
  );
36
44
  }
37
45
  }
@@ -11,7 +11,7 @@ interface Props {
11
11
  Astro.props.sublist.map((entry) => (
12
12
  <li class:list={{ 'sidebar-group': entry.type === 'group' }}>
13
13
  {entry.type === 'link' ? (
14
- <a href={entry.href} aria-current={entry.isCurrent && 'true'}>
14
+ <a href={entry.href} aria-current={entry.isCurrent && 'page'}>
15
15
  {entry.label}
16
16
  </a>
17
17
  ) : (
@@ -56,9 +56,9 @@ interface Props {
56
56
  color: var(--sl-color-white);
57
57
  }
58
58
 
59
- [aria-current='true'],
60
- [aria-current='true']:hover,
61
- [aria-current='true']:focus {
59
+ [aria-current='page'],
60
+ [aria-current='page']:hover,
61
+ [aria-current='page']:focus {
62
62
  font-weight: 600;
63
63
  color: var(--sl-color-text-invert);
64
64
  background-color: var(--sl-color-text-accent);
@@ -0,0 +1,74 @@
1
+ ---
2
+ import { logos } from 'virtual:starlight/user-images';
3
+ import config from 'virtual:starlight/user-config';
4
+
5
+ if (config.logo) {
6
+ let err: string | undefined;
7
+ if ('src' in config.logo) {
8
+ if (!logos.dark || !logos.light) {
9
+ err = `Could not resolve logo import for "${config.logo.src}" (logo.src)`;
10
+ }
11
+ } else {
12
+ if (!logos.dark) {
13
+ err = `Could not resolve logo import for "${config.logo.dark}" (logo.dark)`;
14
+ } else if (!logos.light) {
15
+ err = `Could not resolve logo import for "${config.logo.light}" (logo.light)`;
16
+ }
17
+ }
18
+ if (err) throw new Error(err);
19
+ }
20
+ ---
21
+
22
+ <a href={import.meta.env.BASE_URL} class="site-title flex">
23
+ {
24
+ config.logo && logos.dark && (
25
+ <>
26
+ <img
27
+ class:list={{ 'dark-only': !('src' in config.logo) }}
28
+ alt={config.logo.alt}
29
+ src={logos.dark.src}
30
+ width={logos.dark.width}
31
+ height={logos.dark.height}
32
+ />
33
+ {/* Show light alternate if a user configure both light and dark logos. */}
34
+ {!('src' in config.logo) && (
35
+ <img
36
+ class="light-only"
37
+ alt={config.logo.alt}
38
+ src={logos.light?.src}
39
+ width={logos.light?.width}
40
+ height={logos.light?.height}
41
+ />
42
+ )}
43
+ </>
44
+ )
45
+ }
46
+ <span class:list={{ 'sr-only': config.logo?.replacesTitle }}>
47
+ {config.title}
48
+ </span>
49
+ </a>
50
+
51
+ <style>
52
+ .site-title {
53
+ align-items: center;
54
+ gap: var(--sl-nav-gap);
55
+ font-size: var(--sl-text-h4);
56
+ font-weight: 600;
57
+ color: var(--sl-color-text-accent);
58
+ text-decoration: none;
59
+ white-space: nowrap;
60
+ }
61
+ img {
62
+ height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
63
+ width: auto;
64
+ max-width: 100%;
65
+ object-fit: contain;
66
+ object-position: 0 50%;
67
+ }
68
+ :global([data-theme='light']) .dark-only {
69
+ display: none;
70
+ }
71
+ :global([data-theme='dark']) .light-only {
72
+ display: none;
73
+ }
74
+ </style>
@@ -12,19 +12,25 @@ interface Props {
12
12
  const toc = generateToC(Astro.props.headings, config.tableOfContents);
13
13
  ---
14
14
 
15
- <nav aria-labelledby="starlight__on-this-page--mobile" class="lg:hidden">
16
- <details id="starlight__mobile-toc">
17
- <summary id="starlight__on-this-page--mobile" class="flex">
18
- <div class="toggle flex">
19
- On this page
20
- <Icon name={'right-caret'} class="caret" size="1rem" />
15
+ <mobile-starlight-toc
16
+ data-min-h={config.tableOfContents.minHeadingLevel}
17
+ data-max-h={config.tableOfContents.maxHeadingLevel}
18
+ >
19
+ <nav aria-labelledby="starlight__on-this-page--mobile" class="lg:hidden">
20
+ <details id="starlight__mobile-toc">
21
+ <summary id="starlight__on-this-page--mobile" class="flex">
22
+ <div class="toggle flex">
23
+ On this page
24
+ <Icon name={'right-caret'} class="caret" size="1rem" />
25
+ </div>
26
+ <span class="display-current">{toc[0]?.text}</span>
27
+ </summary>
28
+ <div class="dropdown">
29
+ <TableOfContentsList toc={toc} isMobile />
21
30
  </div>
22
- </summary>
23
- <div class="dropdown">
24
- <TableOfContentsList toc={toc} isMobile />
25
- </div>
26
- </details>
27
- </nav>
31
+ </details>
32
+ </nav>
33
+ </mobile-starlight-toc>
28
34
 
29
35
  <style>
30
36
  nav {
@@ -41,9 +47,12 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
41
47
  }
42
48
 
43
49
  summary {
50
+ gap: 0.5rem;
51
+ align-items: center;
44
52
  height: var(--sl-mobile-toc-height);
45
53
  border-bottom: 1px solid var(--sl-color-hairline-shade);
46
54
  padding: 0.5rem 1rem;
55
+ font-size: var(--sl-text-xs);
47
56
  outline-offset: var(--sl-outline-offset-inside);
48
57
  }
49
58
  summary::marker,
@@ -52,6 +61,7 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
52
61
  }
53
62
 
54
63
  .toggle {
64
+ flex-shrink: 0;
55
65
  gap: 1rem;
56
66
  align-items: center;
57
67
  justify-content: space-between;
@@ -60,7 +70,6 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
60
70
  padding-block: 0.5rem;
61
71
  padding-inline-start: 0.75rem;
62
72
  padding-inline-end: 0.5rem;
63
- font-size: var(--sl-text-xs);
64
73
  line-height: 1;
65
74
  background-color: var(--sl-color-black);
66
75
  user-select: none;
@@ -82,6 +91,13 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
82
91
  transform: rotateZ(90deg);
83
92
  }
84
93
 
94
+ .display-current {
95
+ white-space: nowrap;
96
+ text-overflow: ellipsis;
97
+ overflow: hidden;
98
+ color: var(--sl-color-white);
99
+ }
100
+
85
101
  .dropdown {
86
102
  --border-top: 1px;
87
103
  margin-top: calc(-1 * var(--border-top));
@@ -95,31 +111,43 @@ const toc = generateToC(Astro.props.headings, config.tableOfContents);
95
111
  </style>
96
112
 
97
113
  <script>
98
- const details = document.querySelector<HTMLDetailsElement>(
99
- '#starlight__mobile-toc'
100
- );
101
- if (details) {
102
- const closeToC = () => {
103
- details.open = false;
104
- };
105
- // Close the table of contents whenever a link is clicked.
106
- details.querySelectorAll('a').forEach((a) => {
107
- a.addEventListener('click', closeToC);
108
- });
109
- // Close the table of contents when a user clicks outside of it.
110
- window.addEventListener('click', (e) => {
111
- if (!details.contains(e.target as Node)) closeToC();
112
- });
113
- // Or when they press the escape key.
114
- window.addEventListener('keydown', (e) => {
115
- if (e.key === 'Escape' && details.open) {
116
- const hasFocus = details.contains(document.activeElement);
117
- closeToC();
118
- if (hasFocus) {
119
- const summary = details.querySelector('summary');
120
- if (summary) summary.focus();
114
+ import { StarlightTOC } from './starlight-toc';
115
+
116
+ class MobileStarlightTOC extends StarlightTOC {
117
+ override set current(link: HTMLAnchorElement) {
118
+ super.current = link;
119
+ const display = this.querySelector('.display-current') as HTMLSpanElement;
120
+ if (display) display.textContent = link.textContent;
121
+ }
122
+
123
+ constructor() {
124
+ super();
125
+ const details = this.querySelector('details');
126
+ if (!details) return;
127
+ const closeToC = () => {
128
+ details.open = false;
129
+ };
130
+ // Close the table of contents whenever a link is clicked.
131
+ details.querySelectorAll('a').forEach((a) => {
132
+ a.addEventListener('click', closeToC);
133
+ });
134
+ // Close the table of contents when a user clicks outside of it.
135
+ window.addEventListener('click', (e) => {
136
+ if (!details.contains(e.target as Node)) closeToC();
137
+ });
138
+ // Or when they press the escape key.
139
+ window.addEventListener('keydown', (e) => {
140
+ if (e.key === 'Escape' && details.open) {
141
+ const hasFocus = details.contains(document.activeElement);
142
+ closeToC();
143
+ if (hasFocus) {
144
+ const summary = details.querySelector('summary');
145
+ if (summary) summary.focus();
146
+ }
121
147
  }
122
- }
123
- });
148
+ });
149
+ }
124
150
  }
151
+
152
+ customElements.define('mobile-starlight-toc', MobileStarlightTOC);
125
153
  </script>
@@ -14,7 +14,9 @@ const { toc, isMobile = false, depth = 0 } = Astro.props;
14
14
  {
15
15
  toc.map((heading) => (
16
16
  <li>
17
- <a href={'#' + heading.slug}>{heading.text}</a>
17
+ <a href={'#' + heading.slug} aria-current={heading.current && 'true'}>
18
+ <span>{heading.text}</span>
19
+ </a>
18
20
  {heading.children.length > 0 && (
19
21
  <Astro.self
20
22
  toc={heading.children}
@@ -30,27 +32,52 @@ const { toc, isMobile = false, depth = 0 } = Astro.props;
30
32
  <style define:vars={{ depth }}>
31
33
  ul {
32
34
  padding: 0;
33
- }
34
- ul :global(::marker) {
35
- color: transparent;
35
+ list-style: none;
36
36
  }
37
37
  a {
38
- --pad-inline: 0rem;
38
+ --pad-inline: 0.5rem;
39
39
  display: block;
40
+ border-radius: 0.25rem;
41
+ padding-block: 0.25rem;
40
42
  padding-inline: calc(1rem * var(--depth) + var(--pad-inline))
41
43
  var(--pad-inline);
44
+ line-height: 1.25;
45
+ }
46
+ a[aria-current='true'],
47
+ a[aria-current='true']:hover,
48
+ a[aria-current='true']:focus {
49
+ font-weight: 600;
50
+ color: var(--sl-color-text-invert);
51
+ background-color: var(--sl-color-text-accent);
42
52
  }
43
53
  .isMobile a {
44
54
  --pad-inline: 1rem;
55
+ display: flex;
56
+ justify-content: space-between;
57
+ gap: var(--pad-inline);
45
58
  border-top: 1px solid var(--sl-color-gray-6);
59
+ border-radius: 0;
46
60
  padding-block: 0.5rem;
47
61
  color: var(--sl-color-text);
48
62
  font-size: var(--sl-text-sm);
49
- line-height: 1.25;
50
63
  text-decoration: none;
51
64
  outline-offset: var(--sl-outline-offset-inside);
52
65
  }
53
66
  .isMobile:first-child > li:first-child > a {
54
67
  border-top: 0;
55
68
  }
69
+ .isMobile a[aria-current='true'],
70
+ .isMobile a[aria-current='true']:hover,
71
+ .isMobile a[aria-current='true']:focus {
72
+ color: var(--sl-color-white);
73
+ background-color: unset;
74
+ }
75
+ .isMobile a[aria-current='true']::after {
76
+ content: '';
77
+ width: 1rem;
78
+ background-color: var(--sl-color-text-accent);
79
+ /* Check mark SVG icon */
80
+ -webkit-mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg==');
81
+ mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg==');
82
+ }
56
83
  </style>
@@ -2,6 +2,7 @@ import type { MarkdownHeading } from 'astro';
2
2
 
3
3
  export interface TocItem extends MarkdownHeading {
4
4
  children: TocItem[];
5
+ current?: boolean;
5
6
  }
6
7
 
7
8
  function diveChildren(item: TocItem, depth: number): TocItem[] {
@@ -35,10 +36,7 @@ export function generateToC(
35
36
 
36
37
  for (const heading of headings) {
37
38
  if (toc.length === 0) {
38
- toc.push({
39
- ...heading,
40
- children: [],
41
- });
39
+ toc.push({ ...heading, children: [], current: true });
42
40
  } else {
43
41
  const lastItemInToc = toc.at(-1)!;
44
42
  if (heading.depth < lastItemInToc.depth) {
@@ -46,19 +44,13 @@ export function generateToC(
46
44
  }
47
45
  if (heading.depth === lastItemInToc.depth) {
48
46
  // same depth
49
- toc.push({
50
- ...heading,
51
- children: [],
52
- });
47
+ toc.push({ ...heading, children: [] });
53
48
  } else {
54
49
  // higher depth
55
- // push into children, or children' children alike
50
+ // push into children, or children's children alike
56
51
  const gap = heading.depth - lastItemInToc.depth;
57
52
  const target = diveChildren(lastItemInToc, gap);
58
- target.push({
59
- ...heading,
60
- children: [],
61
- });
53
+ target.push({ ...heading, children: [] });
62
54
  }
63
55
  }
64
56
  }
@@ -0,0 +1,85 @@
1
+ export class StarlightTOC extends HTMLElement {
2
+ private _current = this.querySelector(
3
+ 'a[aria-current="true"]'
4
+ ) as HTMLAnchorElement | null;
5
+ private minH = parseInt(this.dataset.minH || '2', 10);
6
+ private maxH = parseInt(this.dataset.maxH || '3', 10);
7
+
8
+ protected set current(link: HTMLAnchorElement) {
9
+ if (link === this._current) return;
10
+ if (this._current) this._current.removeAttribute('aria-current');
11
+ link.setAttribute('aria-current', 'true');
12
+ this._current = link;
13
+ }
14
+
15
+ constructor() {
16
+ super();
17
+
18
+ /** All the links in the table of contents. */
19
+ const links = [...this.querySelectorAll('a')];
20
+
21
+ /** Test if an element is a table-of-contents heading. */
22
+ const isHeading = (el: Element): el is HTMLHeadingElement => {
23
+ if (el instanceof HTMLHeadingElement) {
24
+ // Special case for page title h1
25
+ if (el.id === 'starlight__overview') return true;
26
+ // Check the heading level is within the user-configured limits for the ToC
27
+ const level = el.tagName[1];
28
+ if (level) {
29
+ const int = parseInt(level, 10);
30
+ if (int >= this.minH && int <= this.maxH) return true;
31
+ }
32
+ }
33
+ return false;
34
+ };
35
+
36
+ /** Walk up the DOM to find the nearest heading. */
37
+ const getElementHeading = (
38
+ el: Element | null
39
+ ): HTMLHeadingElement | null => {
40
+ if (!el) return null;
41
+ const origin = el;
42
+ while (el) {
43
+ if (isHeading(el)) return el;
44
+ // Assign the previous sibling’s last, most deeply nested child to el.
45
+ el = el.previousElementSibling;
46
+ while (el?.lastElementChild) {
47
+ el = el.lastElementChild;
48
+ }
49
+ // Look for headings amongst siblings.
50
+ const h = getElementHeading(el);
51
+ if (h) return h;
52
+ }
53
+ // Walk back up the parent.
54
+ return getElementHeading(origin.parentElement);
55
+ };
56
+
57
+ /** Handle intersections and set the current link to the heading for the current intersection. */
58
+ const setCurrent: IntersectionObserverCallback = (entries) => {
59
+ for (const { isIntersecting, target } of entries) {
60
+ if (!isIntersecting) continue;
61
+ const heading = getElementHeading(target);
62
+ if (!heading) continue;
63
+ const link = links.find((link) => link.hash === '#' + heading.id);
64
+ if (link) {
65
+ this.current = link;
66
+ break;
67
+ }
68
+ }
69
+ };
70
+
71
+ const headingsObserver = new IntersectionObserver(setCurrent, {
72
+ rootMargin: '5% 0% -85%',
73
+ });
74
+
75
+ // Observe elements with an `id` (most likely headings) and their siblings.
76
+ // Also observe direct children of `.content` to include elements before
77
+ // the first heading.
78
+ const toObserve = document.querySelectorAll(
79
+ 'main [id], main [id] ~ *, main .content > *'
80
+ );
81
+ toObserve.forEach((h) => headingsObserver.observe(h));
82
+ }
83
+ }
84
+
85
+ customElements.define('starlight-toc', StarlightTOC);
@@ -11,7 +11,14 @@ interface Props {
11
11
  const toc = generateToC(Astro.props.headings, config.tableOfContents);
12
12
  ---
13
13
 
14
- <nav aria-labelledby="starlight__on-this-page">
15
- <h2 id="starlight__on-this-page">On this page</h2>
16
- <TableOfContentsList toc={toc} />
17
- </nav>
14
+ <starlight-toc
15
+ data-min-h={config.tableOfContents.minHeadingLevel}
16
+ data-max-h={config.tableOfContents.maxHeadingLevel}
17
+ >
18
+ <nav aria-labelledby="starlight__on-this-page">
19
+ <h2 id="starlight__on-this-page">On this page</h2>
20
+ <TableOfContentsList toc={toc} />
21
+ </nav>
22
+ </starlight-toc>
23
+
24
+ <script src="./TableOfContents/starlight-toc"></script>
package/index.astro CHANGED
@@ -47,6 +47,20 @@ const prevNextLinks = getPrevNextLinks(sidebar);
47
47
  <html lang={lang} dir={dir}>
48
48
  <head>
49
49
  <HeadSEO data={entry.data} lang={lang} />
50
+ <style>
51
+ /* Add scroll padding to ensure anchor headings aren't obscured by nav */
52
+ html {
53
+ /* Additional padding is needed to account for the mobile TOC */
54
+ scroll-padding-top: calc(
55
+ 1.5rem + var(--sl-nav-height) + var(--sl-mobile-toc-height)
56
+ );
57
+ }
58
+ @media (min-width: 72em) {
59
+ html {
60
+ scroll-padding-top: calc(1.5rem + var(--sl-nav-height));
61
+ }
62
+ }
63
+ </style>
50
64
  </head>
51
65
  <body>
52
66
  <ThemeProvider />
@@ -57,14 +71,10 @@ const prevNextLinks = getPrevNextLinks(sidebar);
57
71
  <MobileTableOfContents headings={headings} />
58
72
  <TwoColumnContent>
59
73
  <RightSidebar slot="right-sidebar" entry={entry} headings={headings} />
60
- <main
61
- id="starlight__overview"
62
- data-pagefind-body
63
- lang={entryMeta.lang}
64
- dir={entryMeta.dir}
65
- >
74
+ <main data-pagefind-body lang={entryMeta.lang} dir={entryMeta.dir}>
66
75
  <ContentPanel>
67
76
  <h1
77
+ id="starlight__overview"
68
78
  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;"
69
79
  >
70
80
  {entry.data.title}
package/index.ts CHANGED
@@ -45,6 +45,7 @@ export default function StarlightIntegration(
45
45
  ? {}
46
46
  : { theme: 'css-variables' },
47
47
  },
48
+ experimental: { assets: true },
48
49
  };
49
50
  updateConfig(newConfig);
50
51
  },
@@ -84,6 +85,11 @@ function vitePluginStarlightUserConfig(
84
85
  'virtual:starlight/user-css': opts.customCss
85
86
  .map((id) => `import "${id}";`)
86
87
  .join(''),
88
+ 'virtual:starlight/user-images': opts.logo
89
+ ? 'src' in opts.logo
90
+ ? `import src from "${opts.logo.src}"; export const logos = { dark: src, light: src };`
91
+ : `import dark from "${opts.logo.dark}"; import light from "${opts.logo.light}"; export const logos = { dark, light };`
92
+ : 'export const logos = {};',
87
93
  };
88
94
  const resolutionMap = Object.fromEntries(
89
95
  (Object.keys(modules) as (keyof typeof modules)[]).map((key) => [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -0,0 +1,28 @@
1
+ import { z } from 'astro/zod';
2
+
3
+ export const LogoConfigSchema = () =>
4
+ z
5
+ .union([
6
+ z.object({
7
+ /** Source of the image file to use. */
8
+ src: z.string(),
9
+ /** Alternative text description of the logo. */
10
+ alt: z.string().default(''),
11
+ /** Set to `true` to hide the site title text and only show the logo. */
12
+ replacesTitle: z.boolean().default(false),
13
+ }),
14
+ z.object({
15
+ /** Source of the image file to use in dark mode. */
16
+ dark: z.string(),
17
+ /** Source of the image file to use in light mode. */
18
+ light: z.string(),
19
+ /** Alternative text description of the logo. */
20
+ alt: z.string().default(''),
21
+ /** Set to `true` to hide the site title text and only show the logo. */
22
+ replacesTitle: z.boolean().default(false),
23
+ }),
24
+ ])
25
+ .optional();
26
+
27
+ export type LogoUserConfig = z.input<ReturnType<typeof LogoConfigSchema>>;
28
+ export type LogoConfig = z.output<ReturnType<typeof LogoConfigSchema>>;
package/utils/routing.ts CHANGED
@@ -8,8 +8,12 @@ import {
8
8
  slugToParam,
9
9
  } from './slugs';
10
10
 
11
+ export type StarlightDocsEntry = Omit<CollectionEntry<'docs'>, 'slug'> & {
12
+ slug: string;
13
+ };
14
+
11
15
  export interface Route extends LocaleData {
12
- entry: CollectionEntry<'docs'>;
16
+ entry: StarlightDocsEntry;
13
17
  entryMeta: LocaleData;
14
18
  slug: string;
15
19
  isFallback?: true;
@@ -21,8 +25,17 @@ interface Path extends GetStaticPathsItem {
21
25
  props: Route;
22
26
  }
23
27
 
28
+ /**
29
+ * Astro is inconsistent in its `index.md` slug generation. In most cases,
30
+ * `index` is stripped, but in the root of a collection, we get a slug of `index`.
31
+ * We map that to an empty string for consistent behaviour.
32
+ */
33
+ const normalizeIndexSlug = (slug: string) => (slug === 'index' ? '' : slug);
34
+
24
35
  /** All entries in the docs content collection. */
25
- const docs = await getCollection('docs');
36
+ const docs: StarlightDocsEntry[] = (await getCollection('docs')).map(
37
+ ({ slug, ...entry }) => ({ ...entry, slug: normalizeIndexSlug(slug) })
38
+ );
26
39
 
27
40
  function getRoutes(): Route[] {
28
41
  const routes: Route[] = docs.map((entry) => ({
@@ -87,7 +100,7 @@ export function getLocaleRoutes(locale: string | undefined): Route[] {
87
100
  * Get all entries in the docs content collection for a specific locale.
88
101
  * A locale of `undefined` is treated as the “root” locale, if configured.
89
102
  */
90
- function getLocaleDocs(locale: string | undefined): CollectionEntry<'docs'>[] {
103
+ function getLocaleDocs(locale: string | undefined): StarlightDocsEntry[] {
91
104
  return filterByLocale(docs, locale);
92
105
  }
93
106
 
@@ -98,11 +111,16 @@ function filterByLocale<T extends { slug: string }>(
98
111
  ): T[] {
99
112
  if (config.locales) {
100
113
  if (locale && locale in config.locales) {
101
- return items.filter((i) => i.slug.startsWith(locale + '/'));
114
+ return items.filter(
115
+ (i) => i.slug === locale || i.slug.startsWith(locale + '/')
116
+ );
102
117
  } else if (config.locales.root) {
103
118
  const langKeys = Object.keys(config.locales).filter((k) => k !== 'root');
119
+ const isLangIndex = new RegExp(`^(${langKeys.join('|')})$`);
104
120
  const isLangDir = new RegExp(`^(${langKeys.join('|')})/`);
105
- return items.filter((i) => !isLangDir.test(i.slug));
121
+ return items.filter(
122
+ (i) => !isLangIndex.test(i.slug) && !isLangDir.test(i.slug)
123
+ );
106
124
  }
107
125
  }
108
126
  return items;
package/utils/slugs.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { CollectionEntry } from 'astro:content';
2
1
  import config from 'virtual:starlight/user-config';
3
2
 
4
3
  export interface LocaleData {
@@ -16,9 +15,7 @@ export interface LocaleData {
16
15
  * Root locale slugs will return `undefined`.
17
16
  * @param slug A collection entry slug
18
17
  */
19
- function slugToLocale(
20
- slug: CollectionEntry<'docs'>['slug']
21
- ): string | undefined {
18
+ function slugToLocale(slug: string): string | undefined {
22
19
  const locales = Object.keys(config.locales || {});
23
20
  const baseSegment = slug.split('/')[0];
24
21
  if (baseSegment && locales.includes(baseSegment)) return baseSegment;
@@ -26,9 +23,7 @@ function slugToLocale(
26
23
  }
27
24
 
28
25
  /** Get locale information for a given slug. */
29
- export function slugToLocaleData(
30
- slug: CollectionEntry<'docs'>['slug']
31
- ): LocaleData {
26
+ export function slugToLocaleData(slug: string): LocaleData {
32
27
  const locale = slugToLocale(slug);
33
28
  return { dir: localeToDir(locale), lang: localeToLang(locale), locale };
34
29
  }
@@ -56,7 +51,7 @@ function localeToDir(locale: string | undefined): 'ltr' | 'rtl' {
56
51
  }
57
52
 
58
53
  export function slugToParam(slug: string): string | undefined {
59
- return slug === 'index'
54
+ return slug === 'index' || slug === ''
60
55
  ? undefined
61
56
  : slug.endsWith('/index')
62
57
  ? slug.replace('/index', '')
@@ -79,13 +74,16 @@ export function slugToPathname(slug: string): string {
79
74
  * localizedSlug('en/home', undefined) // => 'home'
80
75
  */
81
76
  export function localizedSlug(
82
- slug: CollectionEntry<'docs'>['slug'],
77
+ slug: string,
83
78
  locale: string | undefined
84
79
  ): string {
85
80
  const slugLocale = slugToLocale(slug);
86
81
  if (slugLocale === locale) return slug;
82
+ locale = locale || '';
87
83
  if (slugLocale) {
88
- return slug.replace(slugLocale + '/', locale ? locale + '/' : '');
84
+ return slug
85
+ .replace(slugLocale + '/', locale ? locale + '/' : '')
86
+ .replace(/\/$/, '');
89
87
  }
90
- return locale + '/' + slug;
88
+ return slug ? locale + '/' + slug : locale;
91
89
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'astro/zod';
2
2
  import { parse as bcpParse, stringify as bcpStringify } from 'bcp-47';
3
3
  import { HeadConfigSchema } from '../schemas/head';
4
+ import { LogoConfigSchema } from '../schemas/logo';
4
5
 
5
6
  const LocaleSchema = z.object({
6
7
  /** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */
@@ -101,6 +102,9 @@ const UserConfigSchema = z.object({
101
102
  'Description metadata for your website. Can be used in page metadata.'
102
103
  ),
103
104
 
105
+ /** Set a logo image to show in the navigation bar alongside or instead of the site title. */
106
+ logo: LogoConfigSchema(),
107
+
104
108
  /** Optional details about the social media accounts for this site. */
105
109
  social: z
106
110
  .object({
package/virtual.d.ts CHANGED
@@ -7,3 +7,11 @@ declare module 'virtual:starlight/project-context' {
7
7
  }
8
8
 
9
9
  declare module 'virtual:starlight/user-css' {}
10
+
11
+ declare module 'virtual:starlight/user-images' {
12
+ type ImageMetadata = import('astro').ImageMetadata;
13
+ export const logos: {
14
+ dark?: ImageMetadata;
15
+ light?: ImageMetadata;
16
+ };
17
+ }