starlight-theme-bejamas 0.0.0-canary.0d34eb2
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 +15 -0
- package/package.json +49 -0
- package/src/config.ts +50 -0
- package/src/env.d.ts +16 -0
- package/src/global.d.ts +18 -0
- package/src/index.ts +159 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vite.ts +62 -0
- package/src/overrides/Footer.astro +130 -0
- package/src/overrides/Header.astro +153 -0
- package/src/overrides/Hero.astro +175 -0
- package/src/overrides/MobileTableOfContents.astro +275 -0
- package/src/overrides/PageFrame.astro +187 -0
- package/src/overrides/PageTitle.astro +139 -0
- package/src/overrides/Pagination.astro +55 -0
- package/src/overrides/Sidebar.astro +43 -0
- package/src/overrides/SiteTitle.astro +59 -0
- package/src/overrides/TableOfContents/TableOfContentsList.astro +88 -0
- package/src/overrides/TableOfContents/starlight-toc.ts +125 -0
- package/src/overrides/ThemeProvider.astro +106 -0
- package/src/overrides/ThemeSelect.astro +124 -0
- package/src/styles/theme.css +543 -0
- package/src/virtual-internal.d.ts +40 -0
- package/src/virtual-modules.d.ts +28 -0
- package/src/virtual.d.ts +12 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Button } from "virtual:starlight-theme-bejamas/components/button";
|
|
3
|
+
|
|
4
|
+
import { ArrowRight, ArrowLeft, ArrowUpRight } from "@lucide/astro";
|
|
5
|
+
|
|
6
|
+
const { dir, pagination, template, entry } = Astro.locals.starlightRoute;
|
|
7
|
+
const { prev, next } = pagination;
|
|
8
|
+
const isRtl = dir === "rtl";
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class:list={[
|
|
13
|
+
"sl-bejamas-docs-title",
|
|
14
|
+
entry.data.template === "splash" && "mt-10",
|
|
15
|
+
]}
|
|
16
|
+
>
|
|
17
|
+
<h1 id="_top" class="tracking-tight">
|
|
18
|
+
{Astro.locals.starlightRoute.entry.data.title}
|
|
19
|
+
</h1>
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
entry.data.template !== "splash" && (
|
|
23
|
+
<div class="sl-bejamas-docs-title-buttons">
|
|
24
|
+
{entry.data.figmaUrl && (
|
|
25
|
+
<Button
|
|
26
|
+
variant="outline"
|
|
27
|
+
size="sm"
|
|
28
|
+
as="a"
|
|
29
|
+
href={entry.data.figmaUrl}
|
|
30
|
+
target="_blank"
|
|
31
|
+
>
|
|
32
|
+
<svg
|
|
33
|
+
fill="none"
|
|
34
|
+
height="24"
|
|
35
|
+
viewBox="0 0 38 57"
|
|
36
|
+
width="24"
|
|
37
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
38
|
+
class="text-lg"
|
|
39
|
+
>
|
|
40
|
+
<>
|
|
41
|
+
<path
|
|
42
|
+
d="M19 28.5a9.5 9.5 0 1 1 19 0 9.5 9.5 0 0 1-19 0Z"
|
|
43
|
+
fill="#1ABCFE"
|
|
44
|
+
/>
|
|
45
|
+
<path
|
|
46
|
+
d="M0 47.5A9.5 9.5 0 0 1 9.5 38H19v9.5a9.5 9.5 0 1 1-19 0Z"
|
|
47
|
+
fill="#0ACF83"
|
|
48
|
+
/>
|
|
49
|
+
<path d="M19 0v19h9.5a9.5 9.5 0 1 0 0-19H19Z" fill="#FF7262" />
|
|
50
|
+
<path
|
|
51
|
+
d="M0 9.5A9.5 9.5 0 0 0 9.5 19H19V0H9.5A9.5 9.5 0 0 0 0 9.5Z"
|
|
52
|
+
fill="#F24E1E"
|
|
53
|
+
/>
|
|
54
|
+
<path
|
|
55
|
+
d="M0 28.5A9.5 9.5 0 0 0 9.5 38H19V19H9.5A9.5 9.5 0 0 0 0 28.5Z"
|
|
56
|
+
fill="#A259FF"
|
|
57
|
+
/>
|
|
58
|
+
</>
|
|
59
|
+
</svg>
|
|
60
|
+
Figma
|
|
61
|
+
<ArrowUpRight />
|
|
62
|
+
</Button>
|
|
63
|
+
)}
|
|
64
|
+
{prev && (
|
|
65
|
+
<Button
|
|
66
|
+
as="a"
|
|
67
|
+
href={isRtl ? next.href : prev.href}
|
|
68
|
+
variant="outline"
|
|
69
|
+
size="icon-sm"
|
|
70
|
+
>
|
|
71
|
+
<ArrowLeft class="size-4" />
|
|
72
|
+
</Button>
|
|
73
|
+
)}
|
|
74
|
+
{next && (
|
|
75
|
+
<Button
|
|
76
|
+
as="a"
|
|
77
|
+
href={isRtl ? prev.href : next.href}
|
|
78
|
+
variant="outline"
|
|
79
|
+
size="icon-sm"
|
|
80
|
+
>
|
|
81
|
+
<ArrowRight class="size-4" />
|
|
82
|
+
</Button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
{
|
|
89
|
+
entry.data.description && (
|
|
90
|
+
<p class="sl-bejamas-description">{entry.data.description}</p>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<style>
|
|
96
|
+
@layer bejamas {
|
|
97
|
+
.sl-bejamas-docs-title {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
flex-wrap: wrap;
|
|
101
|
+
margin-top: 0.5rem;
|
|
102
|
+
align-items: center;
|
|
103
|
+
}
|
|
104
|
+
.sl-bejamas-docs-title-buttons {
|
|
105
|
+
gap: 0.5rem;
|
|
106
|
+
display: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (min-width: 50rem) {
|
|
110
|
+
.sl-bejamas-docs-title-buttons {
|
|
111
|
+
display: flex;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.sl-bejamas-description {
|
|
117
|
+
color: var(--muted-foreground);
|
|
118
|
+
text-wrap: balance;
|
|
119
|
+
margin-top: calc(var(--spacing) * 4);
|
|
120
|
+
margin-bottom: calc(var(--spacing) * 2);
|
|
121
|
+
width: 100%;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@layer starlight.core {
|
|
125
|
+
h1 {
|
|
126
|
+
font-size: 1.875rem;
|
|
127
|
+
line-height: 1.2;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
color: var(--sl-color-white);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (min-width: 50rem) {
|
|
133
|
+
h1 {
|
|
134
|
+
font-size: 2.25rem;
|
|
135
|
+
line-height: calc(2.5 / 2.25);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Button } from "virtual:starlight-theme-bejamas/components/button";
|
|
3
|
+
import { ArrowRight, ArrowLeft } from "@lucide/astro";
|
|
4
|
+
|
|
5
|
+
const { dir, pagination } = Astro.locals.starlightRoute;
|
|
6
|
+
const { prev, next } = pagination;
|
|
7
|
+
const isRtl = dir === "rtl";
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div class="sl-flex sl-justify-between print:hidden z-10" dir={dir}>
|
|
11
|
+
{
|
|
12
|
+
prev && (
|
|
13
|
+
<Button as="a" href={prev.href} rel="prev" variant="outline">
|
|
14
|
+
{isRtl ? <ArrowRight class="size-4" /> : <ArrowLeft class="size-4" />}
|
|
15
|
+
{prev.label}
|
|
16
|
+
</Button>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
{
|
|
20
|
+
next && (
|
|
21
|
+
<Button as="a" href={next.href} rel="next" variant="outline">
|
|
22
|
+
{isRtl ? <ArrowLeft class="size-4" /> : <ArrowRight class="size-4" />}
|
|
23
|
+
{next.label}
|
|
24
|
+
</Button>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
@layer starlight.core {
|
|
31
|
+
.pagination-links {
|
|
32
|
+
display: grid;
|
|
33
|
+
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
|
|
34
|
+
gap: 1rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[rel="next"] {
|
|
38
|
+
justify-content: end;
|
|
39
|
+
text-align: end;
|
|
40
|
+
flex-direction: row-reverse;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
svg {
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.page-footer {
|
|
48
|
+
margin-top: calc(var(--spacing) * 6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.sl-justify-between {
|
|
52
|
+
justify-content: space-between;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
import MobileMenuFooter from "virtual:starlight/components/MobileMenuFooter";
|
|
3
|
+
import SidebarPersister from "@astrojs/starlight/components/SidebarPersister.astro";
|
|
4
|
+
import SidebarSublist from "@astrojs/starlight/components/SidebarSublist.astro";
|
|
5
|
+
|
|
6
|
+
const { sidebar } = Astro.locals.starlightRoute;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<SidebarPersister>
|
|
10
|
+
<div class="fade"></div>
|
|
11
|
+
<SidebarSublist sublist={sidebar} />
|
|
12
|
+
</SidebarPersister>
|
|
13
|
+
|
|
14
|
+
<div class="md:sl-hidden">
|
|
15
|
+
<MobileMenuFooter />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<style>
|
|
19
|
+
@layer bejamas {
|
|
20
|
+
.fade {
|
|
21
|
+
--blur: 4px;
|
|
22
|
+
--stop: 50%;
|
|
23
|
+
--height: 72px;
|
|
24
|
+
position: fixed;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: var(--height);
|
|
28
|
+
user-select: none;
|
|
29
|
+
-webkit-user-select: none;
|
|
30
|
+
left: 0;
|
|
31
|
+
-webkit-backdrop-filter: blur(var(--blur));
|
|
32
|
+
backdrop-filter: blur(var(--blur));
|
|
33
|
+
|
|
34
|
+
background: linear-gradient(to top, transparent, var(--background));
|
|
35
|
+
mask-image: linear-gradient(
|
|
36
|
+
to bottom,
|
|
37
|
+
var(--background) var(--stop),
|
|
38
|
+
transparent
|
|
39
|
+
);
|
|
40
|
+
top: 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { logos } from "virtual:starlight/user-images";
|
|
3
|
+
import config from "virtual:starlight/user-config";
|
|
4
|
+
const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<a href={siteTitleHref} class="site-title sl-flex text-sm text-foreground">
|
|
8
|
+
{
|
|
9
|
+
config.logo && logos.dark && (
|
|
10
|
+
<>
|
|
11
|
+
<img
|
|
12
|
+
class:list={{
|
|
13
|
+
"light:sl-hidden print:hidden": !("src" in config.logo),
|
|
14
|
+
}}
|
|
15
|
+
alt={config.logo.alt}
|
|
16
|
+
src={logos.dark.src}
|
|
17
|
+
width={logos.dark.width}
|
|
18
|
+
height={logos.dark.height}
|
|
19
|
+
/>
|
|
20
|
+
{/* Show light alternate if a user configure both light and dark logos. */}
|
|
21
|
+
{!("src" in config.logo) && (
|
|
22
|
+
<img
|
|
23
|
+
class="dark:sl-hidden print:block"
|
|
24
|
+
alt={config.logo.alt}
|
|
25
|
+
src={logos.light?.src}
|
|
26
|
+
width={logos.light?.width}
|
|
27
|
+
height={logos.light?.height}
|
|
28
|
+
/>
|
|
29
|
+
)}
|
|
30
|
+
</>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
<span class:list={{ "sr-only": config.logo?.replacesTitle }} translate="no">
|
|
34
|
+
{siteTitle}
|
|
35
|
+
</span>
|
|
36
|
+
</a>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
@layer starlight.core {
|
|
40
|
+
.site-title {
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: var(--sl-nav-gap);
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
min-width: 0;
|
|
47
|
+
}
|
|
48
|
+
span {
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
}
|
|
51
|
+
img {
|
|
52
|
+
height: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));
|
|
53
|
+
width: auto;
|
|
54
|
+
max-width: 100%;
|
|
55
|
+
object-fit: contain;
|
|
56
|
+
object-position: 0 50%;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { MarkdownHeading } from "astro";
|
|
3
|
+
|
|
4
|
+
interface TocItem extends MarkdownHeading {
|
|
5
|
+
children: TocItem[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
toc: TocItem[];
|
|
10
|
+
depth?: number;
|
|
11
|
+
isMobile?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { toc, isMobile = false, depth = 0 } = Astro.props;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<ul class:list={{ isMobile }}>
|
|
18
|
+
{
|
|
19
|
+
toc.map((heading) => (
|
|
20
|
+
<li>
|
|
21
|
+
<a href={"#" + heading.slug}>
|
|
22
|
+
<span>{heading.text}</span>
|
|
23
|
+
</a>
|
|
24
|
+
{heading.children.length > 0 && (
|
|
25
|
+
<Astro.self
|
|
26
|
+
toc={heading.children}
|
|
27
|
+
depth={depth + 1}
|
|
28
|
+
isMobile={isMobile}
|
|
29
|
+
/>
|
|
30
|
+
)}
|
|
31
|
+
</li>
|
|
32
|
+
))
|
|
33
|
+
}
|
|
34
|
+
</ul>
|
|
35
|
+
|
|
36
|
+
<style define:vars={{ depth }}>
|
|
37
|
+
@layer starlight.core {
|
|
38
|
+
ul {
|
|
39
|
+
padding: 0;
|
|
40
|
+
list-style: none;
|
|
41
|
+
}
|
|
42
|
+
a {
|
|
43
|
+
--pad-inline: 0.5rem;
|
|
44
|
+
display: block;
|
|
45
|
+
border-radius: 0.25rem;
|
|
46
|
+
padding-block: 0.25rem;
|
|
47
|
+
padding-inline: calc(1rem * var(--depth) + var(--pad-inline))
|
|
48
|
+
var(--pad-inline);
|
|
49
|
+
line-height: 1.25;
|
|
50
|
+
}
|
|
51
|
+
a[aria-current="true"] {
|
|
52
|
+
color: var(--sl-color-text-accent);
|
|
53
|
+
}
|
|
54
|
+
.isMobile a {
|
|
55
|
+
--pad-inline: 1rem;
|
|
56
|
+
display: flex;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
gap: var(--pad-inline);
|
|
59
|
+
border-top: 1px solid var(--sl-color-gray-6);
|
|
60
|
+
border-radius: 0;
|
|
61
|
+
padding-block: 0.5rem;
|
|
62
|
+
color: var(--sl-color-text);
|
|
63
|
+
font-size: var(--sl-text-sm);
|
|
64
|
+
text-decoration: none;
|
|
65
|
+
outline-offset: var(--sl-outline-offset-inside);
|
|
66
|
+
}
|
|
67
|
+
.isMobile:first-child > li:first-child > a {
|
|
68
|
+
border-top: 0;
|
|
69
|
+
}
|
|
70
|
+
.isMobile a[aria-current="true"],
|
|
71
|
+
.isMobile a[aria-current="true"]:hover,
|
|
72
|
+
.isMobile a[aria-current="true"]:focus {
|
|
73
|
+
color: var(--sl-color-white);
|
|
74
|
+
background-color: unset;
|
|
75
|
+
}
|
|
76
|
+
.isMobile a[aria-current="true"]::after {
|
|
77
|
+
content: "";
|
|
78
|
+
width: 1rem;
|
|
79
|
+
background-color: var(--sl-color-text-accent);
|
|
80
|
+
/* Check mark SVG icon */
|
|
81
|
+
-webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg==");
|
|
82
|
+
mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNCAxNCc+PHBhdGggZD0nTTEwLjkxNCA0LjIwNmEuNTgzLjU4MyAwIDAgMC0uODI4IDBMNS43NCA4LjU1NyAzLjkxNCA2LjcyNmEuNTk2LjU5NiAwIDAgMC0uODI4Ljg1N2wyLjI0IDIuMjRhLjU4My41ODMgMCAwIDAgLjgyOCAwbDQuNzYtNC43NmEuNTgzLjU4MyAwIDAgMCAwLS44NTdaJy8+PC9zdmc+Cg==");
|
|
83
|
+
-webkit-mask-repeat: no-repeat;
|
|
84
|
+
mask-repeat: no-repeat;
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const PAGE_TITLE_ID = "_top";
|
|
2
|
+
|
|
3
|
+
export class StarlightTOC extends HTMLElement {
|
|
4
|
+
private _current = this.querySelector<HTMLAnchorElement>(
|
|
5
|
+
'a[aria-current="true"]',
|
|
6
|
+
);
|
|
7
|
+
private minH = parseInt(this.dataset.minH || "2", 10);
|
|
8
|
+
private maxH = parseInt(this.dataset.maxH || "3", 10);
|
|
9
|
+
|
|
10
|
+
protected set current(link: HTMLAnchorElement) {
|
|
11
|
+
if (link === this._current) return;
|
|
12
|
+
if (this._current) this._current.removeAttribute("aria-current");
|
|
13
|
+
link.setAttribute("aria-current", "true");
|
|
14
|
+
this._current = link;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private onIdle = (cb: IdleRequestCallback) =>
|
|
18
|
+
(window.requestIdleCallback || ((cb) => setTimeout(cb, 1)))(cb);
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
this.onIdle(() => this.init());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private init = (): void => {
|
|
26
|
+
/** All the links in the table of contents. */
|
|
27
|
+
const links = [...this.querySelectorAll("a")];
|
|
28
|
+
|
|
29
|
+
/** Test if an element is a table-of-contents heading. */
|
|
30
|
+
const isHeading = (el: Element): el is HTMLHeadingElement => {
|
|
31
|
+
if (el instanceof HTMLHeadingElement) {
|
|
32
|
+
// Special case for page title h1
|
|
33
|
+
if (el.id === PAGE_TITLE_ID) return true;
|
|
34
|
+
// Check the heading level is within the user-configured limits for the ToC
|
|
35
|
+
const level = el.tagName[1];
|
|
36
|
+
if (level) {
|
|
37
|
+
const int = parseInt(level, 10);
|
|
38
|
+
if (int >= this.minH && int <= this.maxH) return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Walk up the DOM to find the nearest heading. */
|
|
45
|
+
const getElementHeading = (
|
|
46
|
+
el: Element | null,
|
|
47
|
+
): HTMLHeadingElement | null => {
|
|
48
|
+
if (!el) return null;
|
|
49
|
+
const origin = el;
|
|
50
|
+
while (el) {
|
|
51
|
+
if (isHeading(el)) return el;
|
|
52
|
+
// Assign the previous sibling’s last, most deeply nested child to el.
|
|
53
|
+
el = el.previousElementSibling;
|
|
54
|
+
while (el?.lastElementChild) {
|
|
55
|
+
el = el.lastElementChild;
|
|
56
|
+
}
|
|
57
|
+
// Look for headings amongst siblings.
|
|
58
|
+
const h = getElementHeading(el);
|
|
59
|
+
if (h) return h;
|
|
60
|
+
}
|
|
61
|
+
// Walk back up the parent.
|
|
62
|
+
return getElementHeading(origin.parentElement);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Handle intersections and set the current link to the heading for the current intersection. */
|
|
66
|
+
const setCurrent: IntersectionObserverCallback = (entries) => {
|
|
67
|
+
for (const { isIntersecting, target } of entries) {
|
|
68
|
+
if (!isIntersecting) continue;
|
|
69
|
+
const heading = getElementHeading(target);
|
|
70
|
+
if (!heading) continue;
|
|
71
|
+
const link = links.find(
|
|
72
|
+
(link) => link.hash === "#" + encodeURIComponent(heading.id),
|
|
73
|
+
);
|
|
74
|
+
if (link) {
|
|
75
|
+
this.current = link;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Observe elements with an `id` (most likely headings) and their siblings.
|
|
82
|
+
// Also observe direct children of `.content` to include elements before
|
|
83
|
+
// the first heading.
|
|
84
|
+
const toObserve = document.querySelectorAll(
|
|
85
|
+
"main [id], main [id] ~ *, main .content > *",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
let observer: IntersectionObserver | undefined;
|
|
89
|
+
const observe = () => {
|
|
90
|
+
if (observer) return;
|
|
91
|
+
observer = new IntersectionObserver(setCurrent, {
|
|
92
|
+
rootMargin: this.getRootMargin(),
|
|
93
|
+
});
|
|
94
|
+
toObserve.forEach((h) => observer!.observe(h));
|
|
95
|
+
};
|
|
96
|
+
observe();
|
|
97
|
+
|
|
98
|
+
let timeout: NodeJS.Timeout;
|
|
99
|
+
window.addEventListener("resize", () => {
|
|
100
|
+
// Disable intersection observer while window is resizing.
|
|
101
|
+
if (observer) {
|
|
102
|
+
observer.disconnect();
|
|
103
|
+
observer = undefined;
|
|
104
|
+
}
|
|
105
|
+
clearTimeout(timeout);
|
|
106
|
+
timeout = setTimeout(() => this.onIdle(observe), 200);
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
private getRootMargin(): `-${number}px 0% ${number}px` {
|
|
111
|
+
const navBarHeight =
|
|
112
|
+
document.querySelector("header")?.getBoundingClientRect().height || 0;
|
|
113
|
+
// `<summary>` only exists in mobile ToC, so will fall back to 0 in large viewport component.
|
|
114
|
+
const mobileTocHeight =
|
|
115
|
+
this.querySelector("summary")?.getBoundingClientRect().height || 0;
|
|
116
|
+
/** Start intersections at nav height + 2rem padding. */
|
|
117
|
+
const top = navBarHeight + mobileTocHeight + 32;
|
|
118
|
+
/** End intersections `53px` later. This is slightly more than the maximum `margin-top` in Markdown content. */
|
|
119
|
+
const bottom = top + 53;
|
|
120
|
+
const height = document.documentElement.clientHeight;
|
|
121
|
+
return `-${top}px 0% ${bottom - height}px`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
customElements.define("starlight-toc", StarlightTOC);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{/* This is intentionally inlined to avoid FOUC. */}
|
|
2
|
+
<script is:inline>
|
|
3
|
+
window.StarlightThemeProvider = (() => {
|
|
4
|
+
const themeMetaMedias = [
|
|
5
|
+
"(prefers-color-scheme: light)",
|
|
6
|
+
"(prefers-color-scheme: dark)",
|
|
7
|
+
];
|
|
8
|
+
const fallbackThemeColors = {
|
|
9
|
+
light: "#ffffff",
|
|
10
|
+
dark: "#0f171f",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const themeColors = (() => {
|
|
14
|
+
const html = document.documentElement;
|
|
15
|
+
const prevTheme = html.dataset.theme;
|
|
16
|
+
const prevDark = html.classList.contains("dark");
|
|
17
|
+
|
|
18
|
+
const readBackground = (theme) => {
|
|
19
|
+
html.dataset.theme = theme;
|
|
20
|
+
html.classList.toggle("dark", theme === "dark");
|
|
21
|
+
return (
|
|
22
|
+
getComputedStyle(html).getPropertyValue("--background").trim() ||
|
|
23
|
+
undefined
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const light = readBackground("light") || fallbackThemeColors.light;
|
|
28
|
+
const dark = readBackground("dark") || fallbackThemeColors.dark;
|
|
29
|
+
|
|
30
|
+
if (prevTheme !== undefined && prevTheme !== "") {
|
|
31
|
+
html.dataset.theme = prevTheme;
|
|
32
|
+
html.classList.toggle("dark", prevDark);
|
|
33
|
+
} else {
|
|
34
|
+
html.removeAttribute("data-theme");
|
|
35
|
+
html.classList.toggle("dark", prevDark);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { light, dark };
|
|
39
|
+
})();
|
|
40
|
+
|
|
41
|
+
function ensureThemeMeta(head, media) {
|
|
42
|
+
const baseSelector = `meta[name="theme-color"][media="${media}"]`;
|
|
43
|
+
let meta = head.querySelector(`${baseSelector}[data-theme-managed]`);
|
|
44
|
+
if (meta instanceof HTMLMetaElement) return meta;
|
|
45
|
+
|
|
46
|
+
meta = head.querySelector(baseSelector);
|
|
47
|
+
if (meta instanceof HTMLMetaElement) {
|
|
48
|
+
meta.setAttribute("data-theme-managed", "");
|
|
49
|
+
return meta;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
meta = document.createElement("meta");
|
|
53
|
+
meta.setAttribute("name", "theme-color");
|
|
54
|
+
meta.setAttribute("media", media);
|
|
55
|
+
meta.setAttribute("data-theme-managed", "");
|
|
56
|
+
head.appendChild(meta);
|
|
57
|
+
return meta;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function syncThemeColor(theme) {
|
|
61
|
+
const color = themeColors[theme];
|
|
62
|
+
const head = document.head;
|
|
63
|
+
if (!color || !head) return;
|
|
64
|
+
|
|
65
|
+
themeMetaMedias.forEach((media) => {
|
|
66
|
+
const meta = ensureThemeMeta(head, media);
|
|
67
|
+
meta.setAttribute("content", color);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const storedTheme =
|
|
72
|
+
typeof localStorage !== "undefined" &&
|
|
73
|
+
localStorage.getItem("starlight-theme");
|
|
74
|
+
const theme =
|
|
75
|
+
storedTheme ||
|
|
76
|
+
(window.matchMedia("(prefers-color-scheme: light)").matches
|
|
77
|
+
? "light"
|
|
78
|
+
: "dark");
|
|
79
|
+
const effectiveTheme = theme === "light" ? "light" : "dark";
|
|
80
|
+
document.documentElement.dataset.theme = effectiveTheme;
|
|
81
|
+
document.documentElement.classList.toggle(
|
|
82
|
+
"dark",
|
|
83
|
+
effectiveTheme === "dark",
|
|
84
|
+
);
|
|
85
|
+
syncThemeColor(effectiveTheme);
|
|
86
|
+
return {
|
|
87
|
+
updatePickers(theme = storedTheme || "auto") {
|
|
88
|
+
document
|
|
89
|
+
.querySelectorAll("starlight-theme-select")
|
|
90
|
+
.forEach((picker) => {
|
|
91
|
+
const select = picker.querySelector("select");
|
|
92
|
+
if (select) select.value = theme;
|
|
93
|
+
/** @type {HTMLTemplateElement | null} */
|
|
94
|
+
const tmpl = document.querySelector(`#theme-icons`);
|
|
95
|
+
const newIcon = tmpl && tmpl.content.querySelector("." + theme);
|
|
96
|
+
if (newIcon) {
|
|
97
|
+
const oldIcon = picker.querySelector("svg.label-icon");
|
|
98
|
+
if (oldIcon) {
|
|
99
|
+
oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
})();
|
|
106
|
+
</script>
|