@astrojs/starlight 0.0.4 → 0.0.6
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 +16 -11
- package/CHANGELOG.md +22 -0
- package/README.md +4 -0
- package/components/HeadSEO.astro +86 -42
- package/components/RightSidebarPanel.astro +11 -3
- package/components/SidebarSublist.astro +4 -4
- package/components/TableOfContents/MobileTableOfContents.astro +66 -38
- package/components/TableOfContents/TableOfContentsList.astro +33 -6
- package/components/TableOfContents/generateToC.ts +5 -13
- package/components/TableOfContents/starlight-toc.ts +85 -0
- package/components/TableOfContents.astro +11 -4
- package/index.astro +2 -8
- package/package.json +1 -1
- package/schema.ts +4 -0
- package/schemas/head.ts +29 -0
- package/utils/head.ts +95 -0
- package/utils/user-config.ts +23 -0
package/404.astro
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
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
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
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,27 @@
|
|
|
1
1
|
# @astrojs/starlight
|
|
2
2
|
|
|
3
|
+
## 0.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
- [#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.
|
|
10
|
+
|
|
11
|
+
- [`1028119`](https://github.com/withastro/starlight/commit/10281196aba65075e4ac202dc0f23927c44403ee) Thanks [@delucis](https://github.com/delucis)! - Use default locale in `404.astro`.
|
|
12
|
+
|
|
13
|
+
- [`05f8fd4`](https://github.com/withastro/starlight/commit/05f8fd4c3114e4c25075b35086c5b3e7d0ff49d7) Thanks [@delucis](https://github.com/delucis)! - Include `initial-scale=1` in viewport meta tag.
|
|
14
|
+
|
|
15
|
+
- [#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.
|
|
16
|
+
|
|
17
|
+
- [#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.
|
|
18
|
+
|
|
19
|
+
## 0.0.5
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [#42](https://github.com/withastro/starlight/pull/42) [`c6c1b67`](https://github.com/withastro/starlight/commit/c6c1b6727140a76c42c661f406000cc6e9b175de) Thanks [@delucis](https://github.com/delucis)! - Support setting custom `<head>` tags in config or frontmatter.
|
|
24
|
+
|
|
3
25
|
## 0.0.4
|
|
4
26
|
|
|
5
27
|
### 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
|
package/components/HeadSEO.astro
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import type { CollectionEntry, z } from 'astro:content';
|
|
3
3
|
import config from 'virtual:starlight/user-config';
|
|
4
|
+
import type { HeadConfigSchema } from '../schemas/head';
|
|
5
|
+
import { createHead } from '../utils/head';
|
|
4
6
|
import { localizedUrl } from '../utils/localizedUrl';
|
|
5
7
|
|
|
6
8
|
interface Props {
|
|
@@ -15,48 +17,90 @@ const canonical = Astro.site
|
|
|
15
17
|
: undefined;
|
|
16
18
|
const title = data.title || config.title;
|
|
17
19
|
const description = data.description || config.description;
|
|
18
|
-
---
|
|
19
20
|
|
|
20
|
-
<
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
const headDefaults: z.input<ReturnType<typeof HeadConfigSchema>> = [
|
|
22
|
+
{ tag: 'meta', attrs: { charset: 'utf-8' } },
|
|
23
|
+
{
|
|
24
|
+
tag: 'meta',
|
|
25
|
+
attrs: { name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
26
|
+
},
|
|
27
|
+
{ tag: 'title', content: title },
|
|
28
|
+
{ tag: 'link', attrs: { rel: 'canonical', href: canonical?.href } },
|
|
29
|
+
{ tag: 'meta', attrs: { name: 'generator', content: Astro.generator } },
|
|
30
|
+
// Favicon
|
|
31
|
+
{
|
|
32
|
+
tag: 'link',
|
|
33
|
+
attrs: {
|
|
34
|
+
rel: 'shortcut icon',
|
|
35
|
+
href: import.meta.env.BASE_URL + 'favicon.svg',
|
|
36
|
+
type: 'image/svg+xml',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
// OpenGraph Tags
|
|
40
|
+
{ tag: 'meta', attrs: { property: 'og:title', content: title } },
|
|
41
|
+
{ tag: 'meta', attrs: { property: 'og:type', content: 'article' } },
|
|
42
|
+
{ tag: 'meta', attrs: { property: 'og:url', content: canonical?.href } },
|
|
43
|
+
{ tag: 'meta', attrs: { property: 'og:locale', content: lang } },
|
|
44
|
+
{ tag: 'meta', attrs: { property: 'og:description', content: description } },
|
|
45
|
+
{ tag: 'meta', attrs: { property: 'og:site_name', content: config.title } },
|
|
46
|
+
// Twitter Tags
|
|
47
|
+
{
|
|
48
|
+
tag: 'meta',
|
|
49
|
+
attrs: { name: 'twitter:card', content: 'summary_large_image' },
|
|
50
|
+
},
|
|
51
|
+
{ tag: 'meta', attrs: { name: 'twitter:title', content: title } },
|
|
52
|
+
{ tag: 'meta', attrs: { name: 'twitter:description', content: description } },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
if (description)
|
|
56
|
+
headDefaults.push({
|
|
57
|
+
tag: 'meta',
|
|
58
|
+
attrs: { name: 'description', content: description },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Link to language alternates.
|
|
62
|
+
if (canonical && config.isMultilingual) {
|
|
63
|
+
for (const locale in config.locales) {
|
|
64
|
+
const localeOpts = config.locales[locale];
|
|
65
|
+
if (!localeOpts) continue;
|
|
66
|
+
headDefaults.push({
|
|
67
|
+
tag: 'link',
|
|
68
|
+
attrs: {
|
|
69
|
+
rel: 'alternate',
|
|
70
|
+
hreflang: localeOpts.lang,
|
|
71
|
+
href: localizedUrl(canonical, locale).href,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Link to sitemap, but only when `site` is set.
|
|
78
|
+
if (Astro.site) {
|
|
79
|
+
headDefaults.push({
|
|
80
|
+
tag: 'link',
|
|
81
|
+
attrs: {
|
|
82
|
+
rel: 'sitemap',
|
|
83
|
+
href: import.meta.env.BASE_URL + 'sitemap-index.xml',
|
|
84
|
+
},
|
|
85
|
+
});
|
|
36
86
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<meta property="og:site_name" content={config.title} />
|
|
53
|
-
|
|
54
|
-
<!-- Twitter Tags -->
|
|
55
|
-
<meta name="twitter:card" content="summary_large_image" />
|
|
87
|
+
|
|
88
|
+
// Link to Twitter account if set in Starlight config.
|
|
89
|
+
if (config.social?.twitter) {
|
|
90
|
+
headDefaults.push({
|
|
91
|
+
tag: 'meta',
|
|
92
|
+
attrs: {
|
|
93
|
+
name: 'twitter:site',
|
|
94
|
+
content: new URL(config.social.twitter).pathname,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const head = createHead(headDefaults, config.head, data.head);
|
|
100
|
+
---
|
|
101
|
+
|
|
56
102
|
{
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
)
|
|
103
|
+
head.map(({ tag: Tag, attrs, content }) => (
|
|
104
|
+
<Tag {...attrs} set:html={content} />
|
|
105
|
+
))
|
|
60
106
|
}
|
|
61
|
-
<meta name="twitter:title" content={title} />
|
|
62
|
-
<meta name="twitter:description" content={description} />
|
|
@@ -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-
|
|
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
|
-
|
|
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 && '
|
|
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='
|
|
60
|
-
[aria-current='
|
|
61
|
-
[aria-current='
|
|
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);
|
|
@@ -12,19 +12,25 @@ interface Props {
|
|
|
12
12
|
const toc = generateToC(Astro.props.headings, config.tableOfContents);
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
</
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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}
|
|
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:
|
|
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
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
@@ -46,8 +46,6 @@ const prevNextLinks = getPrevNextLinks(sidebar);
|
|
|
46
46
|
|
|
47
47
|
<html lang={lang} dir={dir}>
|
|
48
48
|
<head>
|
|
49
|
-
<meta charset="utf-8" />
|
|
50
|
-
<meta name="viewport" content="width=device-width" />
|
|
51
49
|
<HeadSEO data={entry.data} lang={lang} />
|
|
52
50
|
</head>
|
|
53
51
|
<body>
|
|
@@ -59,14 +57,10 @@ const prevNextLinks = getPrevNextLinks(sidebar);
|
|
|
59
57
|
<MobileTableOfContents headings={headings} />
|
|
60
58
|
<TwoColumnContent>
|
|
61
59
|
<RightSidebar slot="right-sidebar" entry={entry} headings={headings} />
|
|
62
|
-
<main
|
|
63
|
-
id="starlight__overview"
|
|
64
|
-
data-pagefind-body
|
|
65
|
-
lang={entryMeta.lang}
|
|
66
|
-
dir={entryMeta.dir}
|
|
67
|
-
>
|
|
60
|
+
<main data-pagefind-body lang={entryMeta.lang} dir={entryMeta.dir}>
|
|
68
61
|
<ContentPanel>
|
|
69
62
|
<h1
|
|
63
|
+
id="starlight__overview"
|
|
70
64
|
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;"
|
|
71
65
|
>
|
|
72
66
|
{entry.data.title}
|
package/package.json
CHANGED
package/schema.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'astro/zod';
|
|
2
|
+
import { HeadConfigSchema } from './schemas/head';
|
|
2
3
|
|
|
3
4
|
export function docsSchema() {
|
|
4
5
|
return z.object({
|
|
@@ -19,5 +20,8 @@ export function docsSchema() {
|
|
|
19
20
|
* Can also be set to `false` to disable showing an edit link on this page.
|
|
20
21
|
*/
|
|
21
22
|
editUrl: z.union([z.string().url(), z.boolean()]).optional().default(true),
|
|
23
|
+
|
|
24
|
+
/** Set custom `<head>` tags just for this page. */
|
|
25
|
+
head: HeadConfigSchema(),
|
|
22
26
|
});
|
|
23
27
|
}
|
package/schemas/head.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
|
|
3
|
+
export const HeadConfigSchema = () =>
|
|
4
|
+
z
|
|
5
|
+
.array(
|
|
6
|
+
z.object({
|
|
7
|
+
/** Name of the HTML tag to add to `<head>`, e.g. `'meta'`, `'link'`, or `'script'`. */
|
|
8
|
+
tag: z.enum([
|
|
9
|
+
'title',
|
|
10
|
+
'base',
|
|
11
|
+
'link',
|
|
12
|
+
'style',
|
|
13
|
+
'meta',
|
|
14
|
+
'script',
|
|
15
|
+
'noscript',
|
|
16
|
+
'template',
|
|
17
|
+
]),
|
|
18
|
+
/** Attributes to set on the tag, e.g. `{ rel: 'stylesheet', href: '/custom.css' }`. */
|
|
19
|
+
attrs: z
|
|
20
|
+
.record(z.union([z.string(), z.boolean(), z.undefined()]))
|
|
21
|
+
.default({}),
|
|
22
|
+
/** Content to place inside the tag (optional). */
|
|
23
|
+
content: z.string().default(''),
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
.default([]);
|
|
27
|
+
|
|
28
|
+
export type HeadUserConfig = z.input<ReturnType<typeof HeadConfigSchema>>;
|
|
29
|
+
export type HeadConfig = z.output<ReturnType<typeof HeadConfigSchema>>;
|
package/utils/head.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HeadConfig, HeadConfigSchema, HeadUserConfig } from '../schemas/head';
|
|
2
|
+
|
|
3
|
+
const HeadSchema = HeadConfigSchema();
|
|
4
|
+
|
|
5
|
+
/** Create a fully parsed, merged, and sorted head entry array from multiple sources. */
|
|
6
|
+
export function createHead(defaults: HeadUserConfig, ...heads: HeadConfig[]) {
|
|
7
|
+
let head = HeadSchema.parse(defaults);
|
|
8
|
+
for (const next of heads) {
|
|
9
|
+
head = mergeHead(head, next);
|
|
10
|
+
}
|
|
11
|
+
return sortHead(head);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Test if a head config object contains a matching `<title>` or `<meta>` tag.
|
|
16
|
+
*
|
|
17
|
+
* For example, will return true if `head` already contains
|
|
18
|
+
* `<meta name="description" content="A">` and the passed `tag`
|
|
19
|
+
* is `<meta name="description" content="B">`. Tests against `name`,
|
|
20
|
+
* `property`, and `http-equiv` attributes for `<meta>` tags.
|
|
21
|
+
*/
|
|
22
|
+
function hasTag(head: HeadConfig, entry: HeadConfig[number]): boolean {
|
|
23
|
+
switch (entry.tag) {
|
|
24
|
+
case 'title':
|
|
25
|
+
return head.some(({ tag }) => tag === 'title');
|
|
26
|
+
case 'meta':
|
|
27
|
+
return hasOneOf(head, entry, ['name', 'property', 'http-equiv']);
|
|
28
|
+
default:
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Test if a head config object contains a tag of the same type
|
|
35
|
+
* as `entry` and a matching attribute for one of the passed `keys`.
|
|
36
|
+
*/
|
|
37
|
+
function hasOneOf(
|
|
38
|
+
head: HeadConfig,
|
|
39
|
+
entry: HeadConfig[number],
|
|
40
|
+
keys: string[]
|
|
41
|
+
): boolean {
|
|
42
|
+
const attr = getAttr(keys, entry);
|
|
43
|
+
if (!attr) return false;
|
|
44
|
+
const [key, val] = attr;
|
|
45
|
+
return head.some(({ tag, attrs }) => tag === entry.tag && attrs[key] === val);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Find the first matching key–value pair in a head entry’s attributes. */
|
|
49
|
+
function getAttr(
|
|
50
|
+
keys: string[],
|
|
51
|
+
entry: HeadConfig[number]
|
|
52
|
+
): [key: string, value: string | boolean] | undefined {
|
|
53
|
+
let attr: [string, string | boolean] | undefined;
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
const val = entry.attrs[key];
|
|
56
|
+
if (val) {
|
|
57
|
+
attr = [key, val];
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return attr;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Merge two heads, overwriting entries in the first head that exist in the second. */
|
|
65
|
+
function mergeHead(oldHead: HeadConfig, newHead: HeadConfig) {
|
|
66
|
+
return [...oldHead.filter((tag) => !hasTag(newHead, tag)), ...newHead];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Sort head tags to place important tags first and relegate “SEO” meta tags. */
|
|
70
|
+
function sortHead(head: HeadConfig) {
|
|
71
|
+
return head.sort((a, b) => {
|
|
72
|
+
const aImportance = getImportance(a);
|
|
73
|
+
const bImportance = getImportance(b);
|
|
74
|
+
return aImportance > bImportance ? -1 : bImportance > aImportance ? 1 : 0;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Get the relative importance of a specific head tag. */
|
|
79
|
+
function getImportance(entry: HeadConfig[number]) {
|
|
80
|
+
// 1. Important meta tags.
|
|
81
|
+
if (
|
|
82
|
+
entry.tag === 'meta' &&
|
|
83
|
+
('charset' in entry.attrs ||
|
|
84
|
+
'http-equiv' in entry.attrs ||
|
|
85
|
+
entry.attrs.name === 'viewport')
|
|
86
|
+
) {
|
|
87
|
+
return 100;
|
|
88
|
+
}
|
|
89
|
+
// 2. Page title
|
|
90
|
+
if (entry.tag === 'title') return 90;
|
|
91
|
+
// 3. Anything that isn’t an SEO meta tag.
|
|
92
|
+
if (entry.tag !== 'meta') return 80;
|
|
93
|
+
// 4. SEO meta tags.
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
package/utils/user-config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'astro/zod';
|
|
2
2
|
import { parse as bcpParse, stringify as bcpStringify } from 'bcp-47';
|
|
3
|
+
import { HeadConfigSchema } from '../schemas/head';
|
|
3
4
|
|
|
4
5
|
const LocaleSchema = z.object({
|
|
5
6
|
/** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */
|
|
@@ -193,6 +194,28 @@ const UserConfigSchema = z.object({
|
|
|
193
194
|
/** Configure your site’s sidebar navigation items. */
|
|
194
195
|
sidebar: SidebarGroupSchema.array().optional(),
|
|
195
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Add extra tags to your site’s `<head>`.
|
|
199
|
+
*
|
|
200
|
+
* Can also be set for a single page in a page’s frontmatter.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* // Add Fathom analytics to your site
|
|
204
|
+
* starlight({
|
|
205
|
+
* head: [
|
|
206
|
+
* {
|
|
207
|
+
* tag: 'script',
|
|
208
|
+
* attrs: {
|
|
209
|
+
* src: 'https://cdn.usefathom.com/script.js',
|
|
210
|
+
* 'data-site': 'MY-FATHOM-ID',
|
|
211
|
+
* defer: true,
|
|
212
|
+
* },
|
|
213
|
+
* },
|
|
214
|
+
* ],
|
|
215
|
+
* })
|
|
216
|
+
*/
|
|
217
|
+
head: HeadConfigSchema(),
|
|
218
|
+
|
|
196
219
|
/**
|
|
197
220
|
* Provide CSS files to customize the look and feel of your Starlight site.
|
|
198
221
|
*
|