@grove-dev/starlight 0.1.9

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/components/custom/ContainerSection.astro +58 -0
  4. package/components/custom/LinkButton.astro +141 -0
  5. package/components/custom/dropdown/Dropdown.astro +616 -0
  6. package/components/custom/dropdown/DropdownContent.astro +179 -0
  7. package/components/custom/dropdown/DropdownItem.astro +72 -0
  8. package/components/custom/dropdown/DropdownLabel.astro +31 -0
  9. package/components/custom/dropdown/DropdownSeparator.astro +18 -0
  10. package/components/custom/dropdown/DropdownShortcut.astro +24 -0
  11. package/components/custom/dropdown/DropdownTrigger.astro +54 -0
  12. package/components/custom/dropdown/index.ts +29 -0
  13. package/components/overrides/ContentPanel.astro +1 -0
  14. package/components/overrides/Footer.astro +54 -0
  15. package/components/overrides/Header.astro +93 -0
  16. package/components/overrides/Hero.astro +260 -0
  17. package/components/overrides/MarkdownContent.astro +17 -0
  18. package/components/overrides/PageFrame.astro +121 -0
  19. package/components/overrides/PageSidebar.astro +20 -0
  20. package/components/overrides/PageTitle.astro +130 -0
  21. package/components/overrides/Pagination.astro +101 -0
  22. package/components/overrides/Search.astro +549 -0
  23. package/components/overrides/Sidebar.astro +111 -0
  24. package/components/overrides/SiteTitle.astro +65 -0
  25. package/components/overrides/SocialIcons.astro +47 -0
  26. package/components/overrides/TableOfContents.astro +36 -0
  27. package/components/overrides/ThemeSelect.astro +156 -0
  28. package/components/overrides/TwoColumnContent.astro +75 -0
  29. package/components/overrides/parts/Drawer.astro +232 -0
  30. package/components/overrides/parts/MobileMenuToggle.astro +42 -0
  31. package/components/overrides/parts/NavBar.astro +110 -0
  32. package/components/overrides/parts/SidebarSublist.astro +115 -0
  33. package/components/overrides/parts/toc/TableOfContentsList.astro +71 -0
  34. package/components/overrides/parts/toc/starlight-toc.ts +119 -0
  35. package/core/config/constants.ts +1 -0
  36. package/core/config/expresive-code.ts +63 -0
  37. package/core/config/override.ts +48 -0
  38. package/core/config/schemas.ts +57 -0
  39. package/core/config/vite.ts +20 -0
  40. package/core/plugin.ts +56 -0
  41. package/global.d.ts +5 -0
  42. package/index.ts +3 -0
  43. package/package.json +54 -0
  44. package/schema.ts +24 -0
  45. package/styles/base.css +897 -0
  46. package/styles/layers.css +1 -0
  47. package/styles/theme.css +67 -0
  48. package/user-components.ts +3 -0
  49. package/virtual.d.ts +51 -0
@@ -0,0 +1,110 @@
1
+ ---
2
+ import { AstroError } from 'astro/errors';
3
+ import userConfig from 'virtual:lucode-starlight-config';
4
+ import starlightConfig from 'virtual:starlight/user-config';
5
+ import { getRelativeLocaleUrl } from 'astro:i18n';
6
+
7
+ const currentPath = Astro.url.pathname;
8
+
9
+ const defaultLang =
10
+ starlightConfig.defaultLocale?.lang || starlightConfig.defaultLocale?.locale || 'en';
11
+
12
+ export function getTranslation(
13
+ translations: Record<string, string>,
14
+ link: string,
15
+ description: string
16
+ ) {
17
+ const defaultTranslation = translations[defaultLang];
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
+ }
34
+ ---
35
+
36
+ <nav class="nav-bar">
37
+ {
38
+ userConfig.navLinks?.map((nav) => {
39
+ const absoluteLinkRegex = /^https?:\/\//;
40
+
41
+ const link =
42
+ !absoluteLinkRegex.test(nav.link) && Astro.currentLocale
43
+ ? getRelativeLocaleUrl(Astro.currentLocale, nav.link)
44
+ : nav.link;
45
+ const label =
46
+ typeof nav.label === 'string'
47
+ ? nav.label
48
+ : getTranslation(nav.label, nav.link, 'label');
49
+
50
+ return (
51
+ <a class:list={[{ active: currentPath === link }]} href={link} {...nav.attrs}>
52
+ {label}
53
+ {nav.badge && <span class="badge">{nav.badge}</span>}
54
+ </a>
55
+ );
56
+ })
57
+ }
58
+ </nav>
59
+
60
+ <style>
61
+ .nav-bar {
62
+ font-size: 0.875rem;
63
+ line-height: 1.25rem;
64
+ align-items: center;
65
+ display: flex;
66
+ }
67
+
68
+ @media (min-width: 1280px) {
69
+ .nav-bar {
70
+ gap: 0.375rem;
71
+ }
72
+ }
73
+
74
+ .nav-bar a {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ font-weight: 500;
79
+ min-height: 2rem;
80
+ padding: 0 0.625rem;
81
+ border-radius: calc(var(--radius) - 2px);
82
+ text-decoration: none;
83
+ transition-property:
84
+ color, background-color, border-color, text-decoration-color, fill, stroke;
85
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
86
+ transition-duration: 0.15s;
87
+ color: var(--foreground);
88
+ }
89
+
90
+ .nav-bar a:hover {
91
+ color: var(--accent-foreground);
92
+ background-color: color-mix(in oklab, var(--accent) 80%, transparent);
93
+ }
94
+
95
+ .badge {
96
+ padding-top: calc(var(--spacing) * 0.5);
97
+ padding-bottom: calc(var(--spacing) * 0.5);
98
+ padding-left: calc(var(--spacing) * 1.5);
99
+ padding-right: calc(var(--spacing) * 1.5);
100
+ margin-left: calc(var(--spacing) * 2);
101
+ border-radius: 0.375rem;
102
+ font-size: 0.75rem;
103
+ line-height: 1rem;
104
+ font-weight: 400;
105
+ line-height: 1;
106
+ text-decoration: none;
107
+ background: #adfa1d;
108
+ color: #000000;
109
+ }
110
+ </style>
@@ -0,0 +1,115 @@
1
+ ---
2
+ import type { StarlightRouteData } from '@astrojs/starlight/route-data';
3
+
4
+ interface Props {
5
+ sublist: StarlightRouteData['sidebar'];
6
+ nested?: boolean;
7
+ }
8
+
9
+ const { sublist } = Astro.props;
10
+ ---
11
+
12
+ {
13
+ sublist.map((entry) =>
14
+ entry.type === 'link' ? (
15
+ <a
16
+ href={entry.href}
17
+ aria-current={entry.isCurrent && 'page'}
18
+ class:list={['entry-link', entry.attrs.class]}
19
+ {...entry.attrs}
20
+ >
21
+ <span class="entry-link-inner">
22
+ {entry.label}
23
+ {entry.badge && <span class="entry-badge">{entry.badge.text}</span>}
24
+ </span>
25
+ </a>
26
+ ) : (
27
+ <div class="container-sidebar-entry">
28
+ <h4 class="entry-title">{entry.label}</h4>
29
+ <div class="container-group-link">
30
+ <Astro.self sublist={entry.entries} nested />
31
+ </div>
32
+ </div>
33
+ )
34
+ )
35
+ }
36
+
37
+ <style>
38
+ .container-sidebar-entry {
39
+ gap: 0.25rem;
40
+ flex-direction: column;
41
+ display: flex;
42
+ }
43
+
44
+ .entry-title {
45
+ font-weight: 600;
46
+ font-size: 0.75rem;
47
+ line-height: 1rem;
48
+ padding-top: calc(var(--spacing) * 1);
49
+ padding-bottom: calc(var(--spacing) * 1);
50
+ padding-left: calc(var(--spacing) * 2);
51
+ padding-right: calc(var(--spacing) * 2);
52
+ border-radius: calc(var(--radius) - 2px);
53
+ color: var(--muted-foreground);
54
+ }
55
+
56
+ .container-group-link {
57
+ font-size: 0.875rem;
58
+ line-height: 1.25rem;
59
+ gap: 2px;
60
+ grid-auto-rows: max-content;
61
+ grid-auto-flow: row;
62
+ display: grid;
63
+ }
64
+
65
+ .entry-link {
66
+ color: var(--foreground);
67
+ font-weight: 400;
68
+ align-items: center;
69
+ width: 100%;
70
+ height: 1.875rem;
71
+ display: flex;
72
+ text-decoration: none;
73
+ font-size: 0.8rem;
74
+ line-height: 1.125rem;
75
+ }
76
+
77
+ .entry-link-inner {
78
+ display: flex;
79
+ align-items: center;
80
+ padding-left: calc(var(--spacing) * 2);
81
+ padding-right: calc(var(--spacing) * 2);
82
+ border-radius: calc(var(--radius) - 2px);
83
+ height: 100%;
84
+ transition:
85
+ color 0.15s,
86
+ background-color 0.15s;
87
+ }
88
+
89
+ .entry-link[aria-current='page'] {
90
+ font-weight: 500;
91
+ }
92
+
93
+ .entry-link[aria-current='page'] .entry-link-inner {
94
+ background-color: var(--accent);
95
+ color: var(--accent-foreground);
96
+ }
97
+
98
+ .entry-link:hover:not([aria-current='page']) .entry-link-inner {
99
+ background-color: var(--secondary);
100
+ color: var(--secondary-foreground);
101
+ }
102
+
103
+ .entry-badge {
104
+ color: rgb(0 0 0);
105
+ line-height: 1;
106
+ font-size: 0.75rem;
107
+ padding-top: calc(var(--spacing) * 0.5);
108
+ padding-bottom: calc(var(--spacing) * 0.5);
109
+ padding-left: calc(var(--spacing) * 1.5);
110
+ padding-right: calc(var(--spacing) * 1.5);
111
+ background-color: rgb(173 250 29);
112
+ border-radius: calc(var(--radius) - 2px);
113
+ margin-left: calc(var(--spacing) * 2);
114
+ }
115
+ </style>
@@ -0,0 +1,71 @@
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, depth = 0 } = Astro.props;
15
+ ---
16
+
17
+ <ul>
18
+ {
19
+ toc.map((heading) => (
20
+ <li>
21
+ <a href={'#' + heading.slug}>{heading.text}</a>
22
+ {heading.children.length > 0 && (
23
+ <Astro.self toc={heading.children} depth={depth + 1} />
24
+ )}
25
+ </li>
26
+ ))
27
+ }
28
+ </ul>
29
+
30
+ <style define:vars={{ depth }}>
31
+ ul {
32
+ list-style-type: none;
33
+ margin: 0;
34
+ padding: 0;
35
+ }
36
+ li {
37
+ line-height: 1;
38
+ padding-top: 0;
39
+ margin-top: 0;
40
+ }
41
+
42
+ li + li {
43
+ margin-top: calc(var(--spacing) * 2);
44
+ }
45
+
46
+ a {
47
+ text-decoration-line: none;
48
+ color: var(--muted-foreground);
49
+ display: inline-block;
50
+ text-decoration: inherit;
51
+ font-size: 0.8rem;
52
+ font-weight: 400;
53
+ line-height: 1.125rem;
54
+ }
55
+
56
+ a:hover {
57
+ color: color-mix(in oklab, var(--foreground) 72%, var(--muted-foreground));
58
+ }
59
+
60
+ a[aria-current] {
61
+ color: var(--foreground);
62
+ font-weight: 500;
63
+ }
64
+
65
+ ul > li > ul {
66
+ --pad-inline: calc(var(--spacing) * 2);
67
+ margin-top: calc(var(--spacing) * 2);
68
+ padding-inline: calc((1rem - var(--pad-inline)) / var(--depth) + var(--pad-inline))
69
+ var(--pad-inline);
70
+ }
71
+ </style>
@@ -0,0 +1,119 @@
1
+ /* eslint-disable ts/explicit-function-return-type */
2
+ /* eslint-disable accessor-pairs */
3
+ /* eslint-disable ts/strict-boolean-expressions */
4
+
5
+ import { PAGE_TITLE_ID } from '../../../../core/config/constants';
6
+
7
+ export class StarlightTOC extends HTMLElement {
8
+ private _current = this.querySelector<HTMLAnchorElement>('a[aria-current="true"]');
9
+ private minH = Number.parseInt(this.dataset.minH || '2', 10);
10
+ private maxH = Number.parseInt(this.dataset.maxH || '3', 10);
11
+
12
+ protected set current(link: HTMLAnchorElement) {
13
+ if (link === this._current) return;
14
+ if (this._current) this._current.removeAttribute('aria-current');
15
+ link.setAttribute('aria-current', 'true');
16
+ this._current = link;
17
+ }
18
+
19
+ private onIdle = (cb: IdleRequestCallback) =>
20
+ (window.requestIdleCallback || ((cb) => setTimeout(cb, 1)))(cb);
21
+
22
+ constructor() {
23
+ super();
24
+ this.onIdle(() => this.init());
25
+ }
26
+
27
+ private init = (): void => {
28
+ /** All the links in the table of contents. */
29
+ const links = [...this.querySelectorAll('a')];
30
+
31
+ /** Test if an element is a table-of-contents heading. */
32
+ const isHeading = (el: Element): el is HTMLHeadingElement => {
33
+ if (el instanceof HTMLHeadingElement) {
34
+ // Special case for page title h1
35
+ if (el.id === PAGE_TITLE_ID) return true;
36
+ // Check the heading level is within the user-configured limits for the ToC
37
+ const level = el.tagName[1];
38
+ if (level) {
39
+ const int = Number.parseInt(level, 10);
40
+ if (int >= this.minH && int <= this.maxH) return true;
41
+ }
42
+ }
43
+ return false;
44
+ };
45
+
46
+ /** Walk up the DOM to find the nearest heading. */
47
+ const getElementHeading = (el: Element | null): 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('main [id], main [id] ~ *, main .content > *');
85
+
86
+ let observer: IntersectionObserver | undefined;
87
+ const observe = () => {
88
+ if (observer) return;
89
+ observer = new IntersectionObserver(setCurrent, { rootMargin: this.getRootMargin() });
90
+ toObserve.forEach((h) => observer!.observe(h));
91
+ };
92
+ observe();
93
+
94
+ let timeout: NodeJS.Timeout;
95
+ window.addEventListener('resize', () => {
96
+ // Disable intersection observer while window is resizing.
97
+ if (observer) {
98
+ observer.disconnect();
99
+ observer = undefined;
100
+ }
101
+ clearTimeout(timeout);
102
+ timeout = setTimeout(() => this.onIdle(observe), 200);
103
+ });
104
+ };
105
+
106
+ private getRootMargin(): `-${number}px 0% ${number}px` {
107
+ const navBarHeight = document.querySelector('header')?.getBoundingClientRect().height || 0;
108
+ // `<summary>` only exists in mobile ToC, so will fall back to 0 in large viewport component.
109
+ const mobileTocHeight = this.querySelector('summary')?.getBoundingClientRect().height || 0;
110
+ /** Start intersections at nav height + 2rem padding. */
111
+ const top = navBarHeight + mobileTocHeight + 32;
112
+ /** End intersections `53px` later. This is slightly more than the maximum `margin-top` in Markdown content. */
113
+ const bottom = top + 53;
114
+ const height = document.documentElement.clientHeight;
115
+ return `-${top}px 0% ${bottom - height}px`;
116
+ }
117
+ }
118
+
119
+ customElements.define('starlight-toc', StarlightTOC);
@@ -0,0 +1 @@
1
+ export const PAGE_TITLE_ID = '_top';
@@ -0,0 +1,63 @@
1
+ import type { StarlightExpressiveCodeOptions } from '@astrojs/starlight/expressive-code';
2
+ import type { StarlightUserConfig } from '@astrojs/starlight/types';
3
+
4
+ const createInlineSvgUrl = (svgContents: string): string => {
5
+ const inlineSvg = svgContents.replace(
6
+ /^(\s*<svg)\s+([^>]+)\s*(\/?>)/,
7
+ (_match, tagStart: string, attributes: string, tagEnd: string) => {
8
+ const sanitizedAttributes = attributes.replaceAll(
9
+ /(?:width|height)\s*=\s*(?:(["'])[^"']*\1|\d+)\s*/g,
10
+ ''
11
+ );
12
+
13
+ return `${tagStart} ${sanitizedAttributes.trim()}${tagEnd}`;
14
+ }
15
+ );
16
+
17
+ return `url("data:image/svg+xml,${encodeURIComponent(inlineSvg)}")`;
18
+ };
19
+
20
+ export const expressiveCode = (
21
+ config: StarlightUserConfig
22
+ ): boolean | StarlightExpressiveCodeOptions => {
23
+ const userExpressiveCodeConfig =
24
+ config.expressiveCode === false || config.expressiveCode === true
25
+ ? {}
26
+ : config.expressiveCode;
27
+
28
+ return config.expressiveCode === false
29
+ ? false
30
+ : {
31
+ themes: ['github-dark-default', 'github-light-default'],
32
+ ...userExpressiveCodeConfig,
33
+ styleOverrides: {
34
+ codeBackground: 'var(--code-background)',
35
+ borderWidth: '0px',
36
+ borderRadius: 'calc(var(--radius) + 4px)',
37
+ gutterBorderWidth: '0px',
38
+ ...userExpressiveCodeConfig?.styleOverrides,
39
+ frames: {
40
+ editorBackground: 'var(--code-background)',
41
+ editorActiveTabBackground: 'var(--gray-5)',
42
+ editorActiveTabForeground: 'var(--foreground)',
43
+ editorTabBarBackground: 'var(--gray-6)',
44
+ editorTabBarBorderColor: 'var(--border)',
45
+ editorTabBarBorderBottomColor: 'var(--border)',
46
+ terminalBackground: 'var(--code-background)',
47
+ terminalTitlebarBackground: 'var(--gray-6)',
48
+ terminalTitlebarBorderBottomColor: 'var(--border)',
49
+ terminalTitlebarForeground: 'var(--muted-foreground)',
50
+ shadowColor: 'transparent',
51
+ copyIcon: createInlineSvgUrl(
52
+ `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path></svg>`
53
+ ),
54
+ ...userExpressiveCodeConfig?.styleOverrides?.frames,
55
+ },
56
+ textMarkers: {
57
+ markBackground: 'var(--mark-background)',
58
+ markBorderColor: 'var(--border)',
59
+ ...userExpressiveCodeConfig?.styleOverrides?.textMarkers,
60
+ },
61
+ },
62
+ };
63
+ };
@@ -0,0 +1,48 @@
1
+ import type { HookParameters } from '@astrojs/starlight/types';
2
+ import type { AstroIntegrationLogger } from 'astro';
3
+
4
+ type StarlightUserConfig = HookParameters<'config:setup'>['config'];
5
+ type ComponentOverride = keyof NonNullable<StarlightUserConfig['components']>;
6
+
7
+ export const COMPONENT_OVERRIDES: ComponentOverride[] = [
8
+ 'ThemeSelect',
9
+ 'PageFrame',
10
+ 'Header',
11
+ 'SiteTitle',
12
+ 'Sidebar',
13
+ 'TwoColumnContent',
14
+ 'ContentPanel',
15
+ 'PageTitle',
16
+ 'MarkdownContent',
17
+ 'Hero',
18
+ 'Footer',
19
+ 'SocialIcons',
20
+ 'Pagination',
21
+ 'Search',
22
+ 'TableOfContents',
23
+ 'PageSidebar',
24
+ ];
25
+
26
+ export function override(
27
+ starlightConfig: StarlightUserConfig,
28
+ overrides: ComponentOverride[],
29
+ logger: AstroIntegrationLogger
30
+ ): StarlightUserConfig['components'] {
31
+ const components = { ...starlightConfig.components };
32
+ for (const override of overrides) {
33
+ if (starlightConfig.components?.[override] != null) {
34
+ const fallback = `lucode-starlight/components/overrides/${override}.astro`;
35
+
36
+ logger.warn(
37
+ `A \`<${override}>\` component override is already defined in your Starlight configuration.`
38
+ );
39
+ logger.warn(
40
+ `To use \`lucode-starlight/components\`, either remove this override or manually render the content from \`${fallback}\`.`
41
+ );
42
+ continue;
43
+ }
44
+ components[override] = `lucode-starlight/components/overrides/${override}.astro`;
45
+ }
46
+
47
+ return components;
48
+ }
@@ -0,0 +1,57 @@
1
+ import type { AstroBuiltinAttributes } from 'astro';
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import { z } from 'astro/zod';
4
+
5
+ const linkHTMLAttributesSchema = z.record(
6
+ z.string(),
7
+ z.union([z.string(), z.number(), z.boolean(), z.undefined()])
8
+ ) as z.Schema<Omit<HTMLAttributes<'a'>, keyof AstroBuiltinAttributes | 'children'>>;
9
+
10
+ const LinkItemHTMLAttributesSchema = () => linkHTMLAttributesSchema.default({});
11
+
12
+ export const linkSchema = z.object({
13
+ /**
14
+ * An optional badge to display next to the topic label.
15
+ *
16
+ * This option accepts the same configuration as the Starlight badge sidebar item configuration.
17
+ * @see https://starlight.astro.build/guides/sidebar/#badges
18
+ */
19
+ badge: z.string().optional(),
20
+ /**
21
+ * The topic label visible at the top of the sidebar.
22
+ *
23
+ * The value can be a string, or for multilingual sites, an object with values for each different locale. When using
24
+ * the object form, the keys must be BCP-47 tags (e.g. en, fr, or zh-CN).
25
+ */
26
+ label: z.union([z.string(), z.record(z.string(), z.string())]),
27
+ /**
28
+ * 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
+ *
30
+ * For internal links, the link can either be a page included in the items array or a different page acting as the
31
+ * topic’s landing page.
32
+ */
33
+ link: z.string(),
34
+ /** HTML attributes to add to the link item. */
35
+ attrs: LinkItemHTMLAttributesSchema().optional(),
36
+ });
37
+
38
+ export type Link = z.infer<typeof linkSchema>;
39
+
40
+ export const LucodeStarlightConfigSchema = z.object({
41
+ navLinks: z.array(linkSchema).optional(),
42
+ docs: z
43
+ .object({
44
+ includeAiUtilities: z.boolean().optional().default(false),
45
+ })
46
+ .optional()
47
+ .default({ includeAiUtilities: false }),
48
+ footerText: z
49
+ .string()
50
+ .optional()
51
+ .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). Ported to Astro Starlight by [lucas-labs](https://github.com/lucas-labs).'
53
+ ),
54
+ });
55
+
56
+ export type LucodeStarlightUserConfig = z.input<typeof LucodeStarlightConfigSchema>;
57
+ export type LucodeStarlightConfig = z.output<typeof LucodeStarlightConfigSchema>;
@@ -0,0 +1,20 @@
1
+ import type { ViteUserConfig } from 'astro';
2
+ import type { LucodeStarlightConfig } from './schemas';
3
+
4
+ export function vitePlugin(config: LucodeStarlightConfig): VitePlugin {
5
+ const moduleId = 'virtual:lucode-starlight-config';
6
+ const resolvedModuleId = `\0${moduleId}`;
7
+ const moduleContent = `export default ${JSON.stringify(config)}`;
8
+
9
+ return {
10
+ name: 'vite-plugin-lucode-starlight',
11
+ load(id) {
12
+ return id === resolvedModuleId ? moduleContent : undefined;
13
+ },
14
+ resolveId(id) {
15
+ return id === moduleId ? resolvedModuleId : undefined;
16
+ },
17
+ };
18
+ }
19
+
20
+ type VitePlugin = NonNullable<ViteUserConfig['plugins']>[number];
package/core/plugin.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { StarlightPlugin } from '@astrojs/starlight/types';
2
+ import { override, COMPONENT_OVERRIDES } from './config/override';
3
+ import { expressiveCode } from './config/expresive-code';
4
+ import { vitePlugin } from './config/vite';
5
+ import {
6
+ LucodeStarlightConfigSchema,
7
+ type LucodeStarlightConfig,
8
+ type LucodeStarlightUserConfig,
9
+ } from './config/schemas';
10
+
11
+ const parseConfig = (userConfig?: LucodeStarlightUserConfig): LucodeStarlightConfig => {
12
+ const parsedConfig = LucodeStarlightConfigSchema.safeParse(userConfig ?? {});
13
+
14
+ if (!parsedConfig.success) {
15
+ throw new Error(
16
+ `The provided plugin configuration for lucode-starlight is invalid.\n${parsedConfig.error.issues.map((issue) => issue.message).join('\n')}`
17
+ );
18
+ }
19
+
20
+ return parsedConfig.data;
21
+ };
22
+
23
+ const plugin = (userConfig?: LucodeStarlightUserConfig): StarlightPlugin =>
24
+ ({
25
+ name: 'lucode-starlight',
26
+ hooks: {
27
+ 'config:setup': ({ config, logger, updateConfig, addIntegration }) => {
28
+ updateConfig({
29
+ components: override(config, COMPONENT_OVERRIDES, logger),
30
+ customCss: [
31
+ ...(config.customCss ?? []),
32
+ 'lucode-starlight/styles/layers',
33
+ 'lucode-starlight/styles/theme',
34
+ 'lucode-starlight/styles/base',
35
+ ],
36
+ expressiveCode: expressiveCode(config),
37
+ });
38
+
39
+ addIntegration({
40
+ name: 'lucode-starlight-integration',
41
+ hooks: {
42
+ 'astro:config:setup': ({ updateConfig }) => {
43
+ updateConfig({
44
+ vite: { plugins: [vitePlugin(parseConfig(userConfig))] },
45
+ });
46
+ },
47
+ },
48
+ });
49
+ },
50
+ // 'i18n:setup': function ({ injectTranslations }) {
51
+ // injectTranslations(translations);
52
+ // },
53
+ },
54
+ }) satisfies StarlightPlugin;
55
+
56
+ export { plugin };
package/global.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export declare global {
2
+ var StarlightThemeProvider: {
3
+ updatePickers(theme?: string): void;
4
+ };
5
+ }
package/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { plugin } from './core/plugin';
2
+
3
+ export default plugin;