@grove-dev/starlight 0.6.1 → 0.7.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/LICENSE +21 -0
- package/README.md +77 -78
- package/components/custom/LinkButton.astro +132 -87
- package/components/custom/dropdown/DropdownContent.astro +40 -40
- package/components/custom/dropdown/DropdownItem.astro +6 -1
- package/components/custom/dropdown/DropdownTrigger.astro +2 -2
- package/components/overrides/Hero.astro +30 -22
- package/components/overrides/PageFrame.astro +25 -9
- package/components/overrides/PageSidebar.astro +1 -0
- package/components/overrides/PageTitle.astro +1 -1
- package/components/overrides/Sidebar.astro +38 -1
- package/components/overrides/SocialIcons.astro +3 -1
- package/components/overrides/ThemeSelect.astro +4 -4
- package/components/overrides/TwoColumnContent.astro +1 -1
- package/components/overrides/parts/Drawer.astro +13 -7
- package/components/overrides/parts/NavBar.astro +11 -30
- package/components/overrides/parts/SidebarSublist.astro +73 -2
- package/core/config/docs-schema.ts +58 -0
- package/core/config/override.ts +10 -6
- package/core/config/schemas.ts +29 -8
- package/core/config/vite.ts +4 -4
- package/core/i18n.ts +175 -0
- package/core/plugin.ts +13 -11
- package/core/sidebar.ts +22 -0
- package/package.json +3 -19
- package/schema.ts +31 -0
- package/styles/base.css +35 -7
- package/styles/layers.css +1 -1
- package/styles/theme.css +2 -2
- package/user-components.ts +0 -1
- package/virtual.d.ts +3 -3
- package/THIRD_PARTY_LICENSES.md +0 -68
- package/components/custom/Card.astro +0 -118
|
@@ -2,22 +2,30 @@
|
|
|
2
2
|
import { Icon } from '@astrojs/starlight/components';
|
|
3
3
|
import { Image } from 'astro:assets';
|
|
4
4
|
import { PAGE_TITLE_ID } from '../../core/config/constants';
|
|
5
|
-
import
|
|
5
|
+
import { warnAboutMissingDocsSchemaOnce } from '../../core/config/docs-schema';
|
|
6
6
|
import ContainerSection from '../custom/ContainerSection.astro';
|
|
7
7
|
import LinkButton from '../custom/LinkButton.astro';
|
|
8
8
|
|
|
9
9
|
const { data } = Astro.locals.starlightRoute.entry;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
|
|
11
|
+
warnAboutMissingDocsSchemaOnce(data.hero);
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
title = data.title,
|
|
15
|
+
tagline: heroTagline,
|
|
16
|
+
image,
|
|
17
|
+
announcement,
|
|
18
|
+
actions = [],
|
|
19
|
+
} = data.hero || {};
|
|
20
|
+
|
|
21
|
+
// Falls back on an empty tagline too, not just a missing one.
|
|
15
22
|
const tagline = heroTagline || data.description;
|
|
16
23
|
|
|
24
|
+
const hasImage = !!image;
|
|
17
25
|
const imageAttrs = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
loading: 'eager' as const,
|
|
27
|
+
decoding: 'async' as const,
|
|
28
|
+
alt: image?.alt || '',
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
let darkImage: ImageMetadata | undefined;
|
|
@@ -25,19 +33,19 @@ let lightImage: ImageMetadata | undefined;
|
|
|
25
33
|
let rawHtml: string | undefined;
|
|
26
34
|
|
|
27
35
|
if (image) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
if ('file' in image) {
|
|
37
|
+
darkImage = image.file;
|
|
38
|
+
} else if ('dark' in image) {
|
|
39
|
+
darkImage = image.dark;
|
|
40
|
+
lightImage = image.light;
|
|
41
|
+
} else {
|
|
42
|
+
rawHtml = image.html;
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
---
|
|
38
46
|
|
|
39
47
|
<ContainerSection>
|
|
40
|
-
<div class="hero" data-layout={
|
|
48
|
+
<div class="hero" data-layout={data.hero?.layout} data-has-image={hasImage ? '' : undefined}>
|
|
41
49
|
{
|
|
42
50
|
hasImage && (
|
|
43
51
|
<div class="hero-image">
|
|
@@ -67,12 +75,11 @@ if (image) {
|
|
|
67
75
|
)
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
<h1 id={PAGE_TITLE_ID} data-page-title
|
|
78
|
+
<h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />
|
|
71
79
|
</div>
|
|
72
80
|
|
|
73
81
|
<div class="hero-content-container">
|
|
74
|
-
{tagline && <div class="tagline"
|
|
75
|
-
|
|
82
|
+
{tagline && <div class="tagline" set:html={tagline} />}
|
|
76
83
|
{
|
|
77
84
|
actions.length > 0 && (
|
|
78
85
|
<div class="actions">
|
|
@@ -91,7 +98,8 @@ if (image) {
|
|
|
91
98
|
{...attrs}
|
|
92
99
|
>
|
|
93
100
|
{text}
|
|
94
|
-
{icon?.
|
|
101
|
+
{icon?.type === 'icon' && <Icon name={icon.name} />}
|
|
102
|
+
{icon?.type === 'raw' && <Fragment set:html={icon.html} />}
|
|
95
103
|
</LinkButton>
|
|
96
104
|
)
|
|
97
105
|
)}
|
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import { AstroError } from 'astro/errors';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import userConfig from 'virtual:grove-starlight-config';
|
|
5
|
+
import starlightConfig from 'virtual:starlight/user-config';
|
|
6
|
+
import { createLocaleLookup, resolveLocalizedString } from '../../core/i18n';
|
|
3
7
|
import Drawer from './parts/Drawer.astro';
|
|
4
8
|
|
|
5
|
-
const { hasSidebar } = Astro.locals.starlightRoute;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const { hasSidebar, lang, locale } = Astro.locals.starlightRoute;
|
|
10
|
+
|
|
11
|
+
const localeKeys = createLocaleLookup({
|
|
12
|
+
lang,
|
|
13
|
+
locale: Astro.currentLocale ?? locale,
|
|
14
|
+
defaultLang: starlightConfig.defaultLocale?.lang,
|
|
15
|
+
defaultLocale: starlightConfig.defaultLocale?.locale,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let footerText: string;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
footerText = resolveLocalizedString(userConfig.footerText, localeKeys);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new AstroError(
|
|
24
|
+
error instanceof Error ? error.message : 'Invalid footerText localization.',
|
|
25
|
+
'Update the Grove config so footerText includes a key for the default language.'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
12
28
|
---
|
|
13
29
|
|
|
14
30
|
<div data-slot="layout">
|
|
@@ -39,7 +55,7 @@ const { hasSidebar } = Astro.locals.starlightRoute;
|
|
|
39
55
|
</main>
|
|
40
56
|
|
|
41
57
|
<footer>
|
|
42
|
-
<div data-slot="footer-text"
|
|
58
|
+
<div data-slot="footer-text" set:html={marked.parseInline(footerText)} />
|
|
43
59
|
</footer>
|
|
44
60
|
</div>
|
|
45
61
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Icon } from '@astrojs/starlight/components';
|
|
3
3
|
import { PAGE_TITLE_ID } from '../../core/config/constants';
|
|
4
|
-
import userConfig from 'virtual:
|
|
4
|
+
import userConfig from 'virtual:grove-starlight-config';
|
|
5
5
|
import { Dropdown, DropdownContent, DropdownItem, DropdownTrigger } from '../custom/dropdown';
|
|
6
6
|
import LinkButton from '../custom/LinkButton.astro';
|
|
7
7
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import SidebarPersister from '@astrojs/starlight/components/SidebarPersister.astro';
|
|
2
3
|
import SidebarSublist from './parts/SidebarSublist.astro';
|
|
3
4
|
|
|
4
5
|
const { sidebar } = Astro.locals.starlightRoute;
|
|
@@ -10,12 +11,48 @@ const { sidebar } = Astro.locals.starlightRoute;
|
|
|
10
11
|
<div class="sidebar-fade-top"></div>
|
|
11
12
|
<div class="container-sidebar">
|
|
12
13
|
<div class="sidebar-entries">
|
|
13
|
-
|
|
14
|
+
{/* Keeps collapsed groups open and the scroll position across navigations. */}
|
|
15
|
+
<SidebarPersister>
|
|
16
|
+
<SidebarSublist sublist={sidebar} />
|
|
17
|
+
</SidebarPersister>
|
|
14
18
|
</div>
|
|
15
19
|
<div class="sidebar-fade-bottom"></div>
|
|
16
20
|
</div>
|
|
17
21
|
</div>
|
|
18
22
|
|
|
23
|
+
{/*
|
|
24
|
+
Bring the current page's entry into view inside the sidebar's own scroll
|
|
25
|
+
container. SidebarPersister keeps the scroll position across navigations, but
|
|
26
|
+
on a fresh load (or the first visit to a deep page) the sidebar starts at the
|
|
27
|
+
top, so an entry far down the list renders off-screen with no hint that it is
|
|
28
|
+
selected. Only the container is scrolled -- never the page -- and only when
|
|
29
|
+
the entry is actually outside the visible band.
|
|
30
|
+
*/}
|
|
31
|
+
<script is:inline data-astro-rerun>
|
|
32
|
+
(() => {
|
|
33
|
+
const reveal = () => {
|
|
34
|
+
const container = document.querySelector('.container-sidebar');
|
|
35
|
+
const current = container?.querySelector('[aria-current="page"]');
|
|
36
|
+
if (!container || !current) return;
|
|
37
|
+
|
|
38
|
+
const view = container.getBoundingClientRect();
|
|
39
|
+
const entry = current.getBoundingClientRect();
|
|
40
|
+
if (entry.top >= view.top && entry.bottom <= view.bottom) return;
|
|
41
|
+
|
|
42
|
+
const offset =
|
|
43
|
+
entry.top - view.top - container.clientHeight / 2 + entry.height / 2;
|
|
44
|
+
// Instant, not smooth: this positions the sidebar for the page
|
|
45
|
+
// being loaded, so it should already be correct on first paint
|
|
46
|
+
// rather than animating into place while the reader looks at it.
|
|
47
|
+
container.scrollTop += offset;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Runs after SidebarPersister has restored any stored scroll position.
|
|
51
|
+
requestAnimationFrame(reveal);
|
|
52
|
+
document.addEventListener('astro:page-load', () => requestAnimationFrame(reveal));
|
|
53
|
+
})();
|
|
54
|
+
</script>
|
|
55
|
+
|
|
19
56
|
<style>
|
|
20
57
|
.sidebar {
|
|
21
58
|
scrollbar-width: none;
|
|
@@ -32,7 +32,9 @@ const links = config.social || [];
|
|
|
32
32
|
cursor: pointer;
|
|
33
33
|
color: color-mix(in oklab, var(--foreground) 76%, var(--muted-foreground));
|
|
34
34
|
background-color: transparent;
|
|
35
|
-
transition:
|
|
35
|
+
transition:
|
|
36
|
+
color 0.15s,
|
|
37
|
+
background-color 0.15s;
|
|
36
38
|
text-decoration: none;
|
|
37
39
|
}
|
|
38
40
|
a:hover {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
<starlight-theme-
|
|
5
|
+
<starlight-theme-grove-select>
|
|
6
6
|
<button
|
|
7
7
|
aria-label={Astro.locals.t('themeSelect.accessibleLabel')}
|
|
8
8
|
aria-live="polite"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
<span class="sr-only">Toggle theme</span>
|
|
51
51
|
</button>
|
|
52
|
-
</starlight-theme-
|
|
52
|
+
</starlight-theme-grove-select>
|
|
53
53
|
|
|
54
54
|
<style>
|
|
55
55
|
button {
|
|
@@ -137,8 +137,8 @@
|
|
|
137
137
|
});
|
|
138
138
|
|
|
139
139
|
customElements.define(
|
|
140
|
-
'starlight-theme-
|
|
141
|
-
class
|
|
140
|
+
'starlight-theme-grove-select',
|
|
141
|
+
class StarlightThemeGroveSelect extends HTMLElement {
|
|
142
142
|
constructor() {
|
|
143
143
|
super();
|
|
144
144
|
onThemeChange(loadTheme());
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
import userConfig from 'virtual:
|
|
3
|
-
import
|
|
2
|
+
import userConfig from 'virtual:grove-starlight-config';
|
|
3
|
+
import starlightConfig from 'virtual:starlight/user-config';
|
|
4
4
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
5
|
-
|
|
5
|
+
import { createLocaleLookup, resolveNavLabel } from '../../../core/i18n';
|
|
6
|
+
|
|
7
|
+
const { sidebar, lang, locale } = Astro.locals.starlightRoute;
|
|
6
8
|
|
|
7
9
|
const currentPath = Astro.url.pathname;
|
|
10
|
+
|
|
11
|
+
const localeKeys = createLocaleLookup({
|
|
12
|
+
lang,
|
|
13
|
+
locale: Astro.currentLocale ?? locale,
|
|
14
|
+
defaultLang: starlightConfig.defaultLocale?.lang,
|
|
15
|
+
defaultLocale: starlightConfig.defaultLocale?.locale,
|
|
16
|
+
});
|
|
8
17
|
---
|
|
9
18
|
|
|
10
19
|
<div popover="auto" id="drawer">
|
|
@@ -25,10 +34,7 @@ const currentPath = Astro.url.pathname;
|
|
|
25
34
|
!absoluteLinkRegex.test(nav.link) && Astro.currentLocale
|
|
26
35
|
? getRelativeLocaleUrl(Astro.currentLocale, nav.link)
|
|
27
36
|
: nav.link;
|
|
28
|
-
const label =
|
|
29
|
-
typeof nav.label === 'string'
|
|
30
|
-
? nav.label
|
|
31
|
-
: getTranslation(nav.label, nav.link, 'label');
|
|
37
|
+
const label = resolveNavLabel(nav.label, nav.translations, localeKeys);
|
|
32
38
|
|
|
33
39
|
return (
|
|
34
40
|
<a href={link} {...nav.attrs}>
|
|
@@ -1,36 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
3
|
-
import userConfig from 'virtual:lucode-starlight-config';
|
|
2
|
+
import userConfig from 'virtual:grove-starlight-config';
|
|
4
3
|
import starlightConfig from 'virtual:starlight/user-config';
|
|
5
4
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
|
5
|
+
import { createLocaleLookup, resolveNavLabel } from '../../../core/i18n';
|
|
6
6
|
|
|
7
7
|
const currentPath = Astro.url.pathname;
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
starlightConfig.defaultLocale?.lang || starlightConfig.defaultLocale?.locale || 'en';
|
|
9
|
+
const { lang, locale } = Astro.locals.starlightRoute;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!defaultTranslation) {
|
|
20
|
-
throw new AstroError(
|
|
21
|
-
`The ${description} for "${link}" must have a key for the default language "${defaultLang}".`,
|
|
22
|
-
'Update the Starlight config to include a topic label for the default language.'
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let translation = defaultTranslation;
|
|
27
|
-
|
|
28
|
-
if (Astro.currentLocale) {
|
|
29
|
-
translation = translations[Astro.currentLocale] ?? defaultTranslation;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return translation;
|
|
33
|
-
}
|
|
11
|
+
const localeKeys = createLocaleLookup({
|
|
12
|
+
lang,
|
|
13
|
+
locale: Astro.currentLocale ?? locale,
|
|
14
|
+
defaultLang: starlightConfig.defaultLocale?.lang,
|
|
15
|
+
defaultLocale: starlightConfig.defaultLocale?.locale,
|
|
16
|
+
});
|
|
34
17
|
---
|
|
35
18
|
|
|
36
19
|
<nav class="nav-bar">
|
|
@@ -42,10 +25,8 @@ export function getTranslation(
|
|
|
42
25
|
!absoluteLinkRegex.test(nav.link) && Astro.currentLocale
|
|
43
26
|
? getRelativeLocaleUrl(Astro.currentLocale, nav.link)
|
|
44
27
|
: nav.link;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
? nav.label
|
|
48
|
-
: getTranslation(nav.label, nav.link, 'label');
|
|
28
|
+
|
|
29
|
+
const label = resolveNavLabel(nav.label, nav.translations, localeKeys);
|
|
49
30
|
|
|
50
31
|
return (
|
|
51
32
|
<a class:list={[{ active: currentPath === link }]} href={link} {...nav.attrs}>
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { Icon } from '@astrojs/starlight/components';
|
|
3
|
+
import SidebarRestorePoint from '@astrojs/starlight/components/SidebarRestorePoint.astro';
|
|
2
4
|
import type { StarlightRouteData } from '@astrojs/starlight/route-data';
|
|
5
|
+
import { isSidebarGroupOpen } from '../../../core/sidebar';
|
|
3
6
|
|
|
4
7
|
interface Props {
|
|
5
8
|
sublist: StarlightRouteData['sidebar'];
|
|
6
9
|
nested?: boolean;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
const { sublist } = Astro.props;
|
|
12
|
+
const { sublist, nested = false } = Astro.props;
|
|
10
13
|
---
|
|
11
14
|
|
|
12
15
|
{
|
|
@@ -14,7 +17,7 @@ const { sublist } = Astro.props;
|
|
|
14
17
|
entry.type === 'link' ? (
|
|
15
18
|
<a
|
|
16
19
|
href={entry.href}
|
|
17
|
-
aria-current={entry.isCurrent
|
|
20
|
+
aria-current={entry.isCurrent ? 'page' : undefined}
|
|
18
21
|
class:list={['entry-link', entry.attrs.class]}
|
|
19
22
|
{...entry.attrs}
|
|
20
23
|
>
|
|
@@ -23,6 +26,20 @@ const { sublist } = Astro.props;
|
|
|
23
26
|
{entry.badge && <span class="entry-badge">{entry.badge.text}</span>}
|
|
24
27
|
</span>
|
|
25
28
|
</a>
|
|
29
|
+
) : nested ? (
|
|
30
|
+
<details class="entry-group" open={isSidebarGroupOpen(entry)}>
|
|
31
|
+
<summary class="entry-group-summary">
|
|
32
|
+
<span class="entry-link-inner">
|
|
33
|
+
{entry.label}
|
|
34
|
+
{entry.badge && <span class="entry-badge">{entry.badge.text}</span>}
|
|
35
|
+
</span>
|
|
36
|
+
<Icon name="right-caret" class="entry-group-caret" size="1rem" />
|
|
37
|
+
</summary>
|
|
38
|
+
<SidebarRestorePoint />
|
|
39
|
+
<div class="container-group-link nested">
|
|
40
|
+
<Astro.self sublist={entry.entries} nested />
|
|
41
|
+
</div>
|
|
42
|
+
</details>
|
|
26
43
|
) : (
|
|
27
44
|
<div class="container-sidebar-entry">
|
|
28
45
|
<h4 class="entry-title">{entry.label}</h4>
|
|
@@ -62,6 +79,60 @@ const { sublist } = Astro.props;
|
|
|
62
79
|
display: grid;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
/*
|
|
83
|
+
* Nested levels are indented with a guide line rather than plain padding, so that a reader can
|
|
84
|
+
* follow which group a link belongs to once the tree is more than one level deep.
|
|
85
|
+
*/
|
|
86
|
+
.container-group-link.nested {
|
|
87
|
+
margin-left: calc(var(--spacing) * 2);
|
|
88
|
+
padding-left: calc(var(--spacing) * 2);
|
|
89
|
+
border-left: 1px solid var(--border);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.entry-group {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 2px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.entry-group-summary {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: space-between;
|
|
102
|
+
gap: calc(var(--spacing) * 1);
|
|
103
|
+
height: 1.875rem;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
list-style: none;
|
|
106
|
+
color: var(--foreground);
|
|
107
|
+
font-size: 0.8rem;
|
|
108
|
+
line-height: 1.125rem;
|
|
109
|
+
border-radius: calc(var(--radius) - 2px);
|
|
110
|
+
transition:
|
|
111
|
+
color 0.15s,
|
|
112
|
+
background-color 0.15s;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Safari still paints its own marker without this. */
|
|
116
|
+
.entry-group-summary::-webkit-details-marker {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.entry-group-summary:hover {
|
|
121
|
+
background-color: var(--secondary);
|
|
122
|
+
color: var(--secondary-foreground);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.entry-group-caret {
|
|
126
|
+
flex-shrink: 0;
|
|
127
|
+
margin-right: calc(var(--spacing) * 2);
|
|
128
|
+
color: var(--muted-foreground);
|
|
129
|
+
transition: transform 0.15s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.entry-group[open] > .entry-group-summary .entry-group-caret {
|
|
133
|
+
transform: rotate(90deg);
|
|
134
|
+
}
|
|
135
|
+
|
|
65
136
|
.entry-link {
|
|
66
137
|
color: var(--foreground);
|
|
67
138
|
font-weight: 400;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starlight validates frontmatter with `z.object`, which strips keys it does not know about. When
|
|
3
|
+
* `docsSchema()` is called without `extend: ExtendDocsSchema`, the theme's own `hero` fields are
|
|
4
|
+
* therefore dropped without any error, and splash pages silently fall back to the default layout.
|
|
5
|
+
*
|
|
6
|
+
* Detecting that from the parsed frontmatter is unreliable: Starlight builds some entries in code
|
|
7
|
+
* rather than through the schema — its fallback 404 route hardcodes `hero` — so those look stripped
|
|
8
|
+
* even on a correctly configured site. Instead, `@grove-dev/starlight/schema` records that it was
|
|
9
|
+
* imported, which answers the actual question: did the user wire the extension up at all?
|
|
10
|
+
*/
|
|
11
|
+
const LOADED = Symbol.for('@grove-dev/starlight.docs-schema-loaded');
|
|
12
|
+
|
|
13
|
+
/** Called by `@grove-dev/starlight/schema` on import. */
|
|
14
|
+
export function markDocsSchemaLoaded(): void {
|
|
15
|
+
(globalThis as Record<symbol, unknown>)[LOADED] = true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isDocsSchemaExtended(): boolean {
|
|
19
|
+
return (globalThis as Record<symbol, unknown>)[LOADED] === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let warned = false;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Emits {@link missingDocsSchemaWarning} the first time a page with a hero is rendered on a site
|
|
26
|
+
* that never imported the schema. Returns whether it warned; later calls are no-ops, so a site with
|
|
27
|
+
* several splash pages does not repeat the same advice once per page.
|
|
28
|
+
*/
|
|
29
|
+
export function warnAboutMissingDocsSchemaOnce(
|
|
30
|
+
hero: unknown,
|
|
31
|
+
warn: (message: string) => void = console.warn
|
|
32
|
+
): boolean {
|
|
33
|
+
if (warned || hero == null || isDocsSchemaExtended()) return false;
|
|
34
|
+
|
|
35
|
+
warned = true;
|
|
36
|
+
warn(missingDocsSchemaWarning());
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Test seam: clears the once-per-process guard used by {@link warnAboutMissingDocsSchemaOnce}. */
|
|
41
|
+
export function resetDocsSchemaWarning(): void {
|
|
42
|
+
warned = false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The advice printed when the schema extension is missing. */
|
|
46
|
+
export function missingDocsSchemaWarning(): string {
|
|
47
|
+
return [
|
|
48
|
+
'[@grove-dev/starlight] This page sets `hero` frontmatter, but the docs schema is not extended,',
|
|
49
|
+
"so Starlight is dropping the theme's hero fields. `hero.layout`, `hero.announcement` and the",
|
|
50
|
+
'extra `hero.actions[].variant` values have no effect until you extend it:',
|
|
51
|
+
'',
|
|
52
|
+
" import { ExtendDocsSchema } from '@grove-dev/starlight/schema';",
|
|
53
|
+
'',
|
|
54
|
+
' schema: docsSchema({ extend: ExtendDocsSchema }),',
|
|
55
|
+
'',
|
|
56
|
+
'See https://withgrove.dev/reference/plugin-api/#frontmatter-extension',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
package/core/config/override.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HookParameters } from '@astrojs/starlight/types';
|
|
2
2
|
import type { AstroIntegrationLogger } from 'astro';
|
|
3
|
+
import type { GroveStarlightConfig } from './schemas';
|
|
3
4
|
|
|
4
5
|
type StarlightUserConfig = HookParameters<'config:setup'>['config'];
|
|
5
6
|
type ComponentOverride = keyof NonNullable<StarlightUserConfig['components']>;
|
|
@@ -25,6 +26,7 @@ export const COMPONENT_OVERRIDES: ComponentOverride[] = [
|
|
|
25
26
|
|
|
26
27
|
export function override(
|
|
27
28
|
starlightConfig: StarlightUserConfig,
|
|
29
|
+
pluginConfig: GroveStarlightConfig,
|
|
28
30
|
overrides: ComponentOverride[],
|
|
29
31
|
logger: AstroIntegrationLogger
|
|
30
32
|
): StarlightUserConfig['components'] {
|
|
@@ -33,12 +35,14 @@ export function override(
|
|
|
33
35
|
if (starlightConfig.components?.[override] != null) {
|
|
34
36
|
const fallback = `@grove-dev/starlight/components/overrides/${override}.astro`;
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (pluginConfig.warnOverrides) {
|
|
39
|
+
logger.warn(
|
|
40
|
+
`A \`<${override}>\` component override is already defined in your Starlight configuration.`
|
|
41
|
+
);
|
|
42
|
+
logger.warn(
|
|
43
|
+
`To use \`@grove-dev/starlight/components\`, either remove this override or manually render the content from \`${fallback}\`.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
42
46
|
continue;
|
|
43
47
|
}
|
|
44
48
|
components[override] = `@grove-dev/starlight/components/overrides/${override}.astro`;
|
package/core/config/schemas.ts
CHANGED
|
@@ -18,12 +18,21 @@ export const linkSchema = z.object({
|
|
|
18
18
|
*/
|
|
19
19
|
badge: z.string().optional(),
|
|
20
20
|
/**
|
|
21
|
-
* The
|
|
21
|
+
* The link label.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* - A string used as the default-locale label (pair with `translations` for other languages).
|
|
24
|
+
* - Or a locale map keyed by BCP-47 tags / locale paths (e.g. `en`, `es`).
|
|
25
|
+
*
|
|
26
|
+
* @see https://starlight.astro.build/guides/sidebar/#internationalization
|
|
25
27
|
*/
|
|
26
28
|
label: z.union([z.string(), z.record(z.string(), z.string())]),
|
|
29
|
+
/**
|
|
30
|
+
* Optional labels for other languages when `label` is a string.
|
|
31
|
+
* Keys should be BCP-47 tags (e.g. `en`, `es`), matching Starlight sidebar translations.
|
|
32
|
+
*
|
|
33
|
+
* @see https://starlight.astro.build/guides/sidebar/#internationalization
|
|
34
|
+
*/
|
|
35
|
+
translations: z.record(z.string(), z.string()).optional(),
|
|
27
36
|
/**
|
|
28
37
|
* The link to the topic’s content which an be a relative link to local files or the full URL of an external page.
|
|
29
38
|
*
|
|
@@ -37,7 +46,8 @@ export const linkSchema = z.object({
|
|
|
37
46
|
|
|
38
47
|
export type Link = z.infer<typeof linkSchema>;
|
|
39
48
|
|
|
40
|
-
export const
|
|
49
|
+
export const GroveStarlightConfigSchema = z.object({
|
|
50
|
+
/** Array of navigation links for the header/nav bar. */
|
|
41
51
|
navLinks: z.array(linkSchema).optional(),
|
|
42
52
|
docs: z
|
|
43
53
|
.object({
|
|
@@ -45,13 +55,24 @@ export const LucodeStarlightConfigSchema = z.object({
|
|
|
45
55
|
})
|
|
46
56
|
.optional()
|
|
47
57
|
.default({ includeAiUtilities: false }),
|
|
58
|
+
/**
|
|
59
|
+
* Whether to warn when a component override defined in your Starlight configuration prevents
|
|
60
|
+
* the theme from applying its own. Set to `false` to silence those warnings.
|
|
61
|
+
*/
|
|
62
|
+
warnOverrides: z.boolean().optional().default(true),
|
|
63
|
+
/**
|
|
64
|
+
* Footer Markdown text. Can be a string, or for multilingual sites an object with values for
|
|
65
|
+
* each locale. Keys may be BCP-47 tags (e.g. `en`, `es`) or locale paths.
|
|
66
|
+
*
|
|
67
|
+
* @see https://starlight.astro.build/reference/configuration/#title
|
|
68
|
+
*/
|
|
48
69
|
footerText: z
|
|
49
|
-
.string()
|
|
70
|
+
.union([z.string(), z.record(z.string(), z.string())])
|
|
50
71
|
.optional()
|
|
51
72
|
.default(
|
|
52
|
-
'Inspired by the [shadcn/ui](https://ui.shadcn.com/) documentation theme and based on [starlight-theme-black](https://github.com/adrian-ub/starlight-theme-black).
|
|
73
|
+
'Inspired by the [shadcn/ui](https://ui.shadcn.com/) documentation theme and based on [starlight-theme-black](https://github.com/adrian-ub/starlight-theme-black). Originally forked from [lucas-labs/lucode-starlight-theme](https://github.com/lucas-labs/lucode-starlight-theme) and maintained by [grove](https://github.com/tortuvshin/grove).'
|
|
53
74
|
),
|
|
54
75
|
});
|
|
55
76
|
|
|
56
|
-
export type
|
|
57
|
-
export type
|
|
77
|
+
export type GroveStarlightUserConfig = z.input<typeof GroveStarlightConfigSchema>;
|
|
78
|
+
export type GroveStarlightConfig = z.output<typeof GroveStarlightConfigSchema>;
|
package/core/config/vite.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ViteUserConfig } from 'astro';
|
|
2
|
-
import type {
|
|
2
|
+
import type { GroveStarlightConfig } from './schemas';
|
|
3
3
|
|
|
4
|
-
export function vitePlugin(config:
|
|
5
|
-
const moduleId = 'virtual:
|
|
4
|
+
export function vitePlugin(config: GroveStarlightConfig): VitePlugin {
|
|
5
|
+
const moduleId = 'virtual:grove-starlight-config';
|
|
6
6
|
const resolvedModuleId = `\0${moduleId}`;
|
|
7
7
|
const moduleContent = `export default ${JSON.stringify(config)}`;
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
|
-
name: 'vite-plugin-
|
|
10
|
+
name: 'vite-plugin-grove-starlight',
|
|
11
11
|
load(id) {
|
|
12
12
|
return id === resolvedModuleId ? moduleContent : undefined;
|
|
13
13
|
},
|