starlight-theme-celestia 0.2.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.
Files changed (68) hide show
  1. package/lib/aside.css +13 -0
  2. package/lib/badge.css +23 -0
  3. package/lib/card.css +42 -0
  4. package/lib/fonts.css +4 -0
  5. package/lib/layer.css +31 -0
  6. package/lib/markdown.css +71 -0
  7. package/lib/shiki.css +79 -0
  8. package/lib/styles.css +6 -0
  9. package/lib/tailwind.build.css +2 -0
  10. package/lib/tailwind.css +29 -0
  11. package/lib/tailwind.gen.css +1197 -0
  12. package/lib/theme.css +94 -0
  13. package/package.json +56 -0
  14. package/src/astro.d.ts +6 -0
  15. package/src/components/Code.astro +12 -0
  16. package/src/components/CodeCopy.astro +39 -0
  17. package/src/components/CodeFile.astro +64 -0
  18. package/src/components/CodePackageManagers.astro +55 -0
  19. package/src/components/CodeTabItem.astro +13 -0
  20. package/src/components/CodeTabs.astro +67 -0
  21. package/src/components/Head.astro +7 -0
  22. package/src/components/Header.astro +91 -0
  23. package/src/components/Hero.astro +169 -0
  24. package/src/components/LanguageSelect.astro +5 -0
  25. package/src/components/LinkButton.astro +47 -0
  26. package/src/components/LinkButtonIcon.astro +22 -0
  27. package/src/components/LinkCard.astro +32 -0
  28. package/src/components/MarkdownContent.astro +22 -0
  29. package/src/components/MobileMenuFooter.astro +11 -0
  30. package/src/components/MobileMenuToggle.astro +23 -0
  31. package/src/components/MobileTableOfContents.astro +26 -0
  32. package/src/components/PageFrame.astro +50 -0
  33. package/src/components/Pagination.astro +34 -0
  34. package/src/components/PaginationLink.astro +30 -0
  35. package/src/components/Search.astro +645 -0
  36. package/src/components/SiteTitle.astro +47 -0
  37. package/src/components/SiteTitleVersioned.astro +62 -0
  38. package/src/components/SocialIcons.astro +31 -0
  39. package/src/components/ThemeProvider.astro +102 -0
  40. package/src/components/ThemeSelect.astro +188 -0
  41. package/src/components/TwoColumnContent.astro +44 -0
  42. package/src/components/VersionSelect.astro +261 -0
  43. package/src/components/get-i18n-text.ts +39 -0
  44. package/src/components/index.ts +7 -0
  45. package/src/components/mobile-menu-toggle.ts +49 -0
  46. package/src/components/rehype-code-tabs.ts +52 -0
  47. package/src/constants.ts +7 -0
  48. package/src/content.config.ts +16 -0
  49. package/src/content.d.ts +22 -0
  50. package/src/icons/lucide-arrow-left.svg +1 -0
  51. package/src/icons/lucide-arrow-right.svg +1 -0
  52. package/src/icons/lucide-check.svg +1 -0
  53. package/src/icons/lucide-chevron-left.svg +1 -0
  54. package/src/icons/lucide-chevron-right.svg +1 -0
  55. package/src/icons/lucide-clipboard.svg +1 -0
  56. package/src/icons/lucide-menu.svg +1 -0
  57. package/src/icons/lucide-search.svg +1 -0
  58. package/src/icons/lucide-x.svg +1 -0
  59. package/src/icons/tabler-moon-filled.svg +1 -0
  60. package/src/icons/tabler-sun-high-filled.svg +1 -0
  61. package/src/index.ts +112 -0
  62. package/src/shiki-config.ts +49 -0
  63. package/src/shiki-meta-title.ts +6 -0
  64. package/src/shiki-transformer-container.ts +85 -0
  65. package/src/types.d.ts +2 -0
  66. package/src/user-options.ts +37 -0
  67. package/src/virtual-internal.d.ts +44 -0
  68. package/src/virtual-user-config.ts +44 -0
@@ -0,0 +1,39 @@
1
+ import type { StarlightRouteData } from "@astrojs/starlight/route-data";
2
+ import config from "virtual:starlight/user-config";
3
+
4
+ export function getI18nText(value: string | Record<string, string>, route: StarlightRouteData): string {
5
+ if (typeof value === "string") {
6
+ return value;
7
+ }
8
+
9
+ const { lang, locale } = route;
10
+
11
+ // Try `lang` first, which is a BCP-47 language tag
12
+ if (value[lang]) {
13
+ return value[lang];
14
+ }
15
+
16
+ // Use the same "root" convention as the Starlight docs to get the default value.
17
+ // https://github.com/withastro/starlight/blob/9d3ba179c5d524c1c61d771ceb1a7b4e754bee16/docs/src/content/docs/guides/i18n.mdx?plain=1#L72-L75
18
+ if (config.defaultLocale.lang === lang || config.defaultLocale.locale === locale) {
19
+ if (value["root"]) {
20
+ return value["root"];
21
+ }
22
+ }
23
+
24
+ const message =
25
+ `[starlight-theme-celestia] Unable to find the translation for language "${lang}".\n` +
26
+ `Are you using the correct BCP-47 language tag ` +
27
+ `(e.g. "en", "ar", or "zh-CN") in the following configuration?\n` +
28
+ JSON.stringify(value, null, 2) +
29
+ "\n";
30
+
31
+ // Fallback to the `locale`, which is a base path at which a language is
32
+ // served. This is not of the same format as the `lang` BCP-47 language tag.
33
+ if (locale && value[locale]) {
34
+ console.warn(message);
35
+ return value[locale];
36
+ }
37
+
38
+ throw new Error(message);
39
+ }
@@ -0,0 +1,7 @@
1
+ export { default as Code } from "./Code.astro";
2
+ export { default as CodeFile } from "./CodeFile.astro";
3
+ export { default as CodePackageManagers } from "./CodePackageManagers.astro";
4
+ export { default as CodeTabItem } from "./CodeTabItem.astro";
5
+ export { default as CodeTabs } from "./CodeTabs.astro";
6
+ export { default as LinkButton } from "./LinkButton.astro";
7
+ export { default as LinkCard } from "./LinkCard.astro";
@@ -0,0 +1,49 @@
1
+ import type { HostElement } from "@aria-ui/core";
2
+ import { createSignal, defineCustomElement, useEffect } from "@aria-ui/core";
3
+ import { useEventListener } from "@aria-ui/utils";
4
+
5
+ function setupMobileMenuToggle(host: HostElement) {
6
+ const { get: getExpanded, set: setExpanded } = createSignal(false);
7
+
8
+ const toggleExpanded = () => setExpanded(!getExpanded());
9
+
10
+ useEffect(host, () => {
11
+ const expanded: boolean = getExpanded();
12
+ host.setAttribute("aria-controls", expanded ? "true" : "false");
13
+ document.body.toggleAttribute("data-mobile-menu-expanded", expanded || false);
14
+ });
15
+
16
+ useEventListener(host, "click", toggleExpanded);
17
+ useEventListener(host, "keydown", (event) => {
18
+ if (!event.isComposing && (event.key === "Enter" || event.key === " ")) {
19
+ event.preventDefault();
20
+ toggleExpanded();
21
+ }
22
+ });
23
+
24
+ useEffect(host, () => {
25
+ const sidebarNav = document.getElementById("celestia-sidebar-nav");
26
+ if (!sidebarNav) return;
27
+ const handleKeyup = (event: KeyboardEvent) => {
28
+ if (event.code === "Escape") {
29
+ setExpanded(false);
30
+ host.focus();
31
+ }
32
+ };
33
+ sidebarNav.addEventListener("keyup", handleKeyup);
34
+ return () => sidebarNav.removeEventListener("keyup", handleKeyup);
35
+ });
36
+
37
+ useEffect(host, () => {
38
+ host.setAttribute("role", "button");
39
+ host.setAttribute("tabindex", "0");
40
+ });
41
+ }
42
+
43
+ const MobileMenuToggle = defineCustomElement(setupMobileMenuToggle, {});
44
+
45
+ function register() {
46
+ customElements.define("celestia-mobile-menu-toggle", MobileMenuToggle);
47
+ }
48
+
49
+ export { register };
@@ -0,0 +1,52 @@
1
+ import type { Element, ElementContent } from "hast";
2
+ import { rehype } from "rehype";
3
+
4
+ import { CODE_PROPERTY_CONTAINER, CODE_PROPERTY_TITLE } from "../constants";
5
+
6
+ // https://github.com/withastro/starlight/blob/bdb05e704a6cf06a029367f99b6bf2f575e5a5f3/packages/starlight/user-components/rehype-tabs.ts#L20
7
+ export const TabItemTagname = "starlight-tab-item";
8
+
9
+ /**
10
+ * Rehype processor to extract tab panel data and turn each
11
+ * `<starlight-tab-item>` into a `<div>` with the necessary
12
+ * attributes.
13
+ */
14
+ const tabsProcessor = rehype()
15
+ .data("settings", { fragment: true })
16
+ .use(() => {
17
+ return (tree: Element) => {
18
+ return {
19
+ ...tree,
20
+ children: tree.children.map(replacePre),
21
+ };
22
+ };
23
+ });
24
+
25
+ function replacePre(node: ElementContent): ElementContent {
26
+ if (node.type === "element" && node.tagName === "div" && node.properties[CODE_PROPERTY_CONTAINER] != null) {
27
+ const title = node.properties[CODE_PROPERTY_TITLE];
28
+
29
+ if (title) {
30
+ node.properties[CODE_PROPERTY_TITLE] = undefined;
31
+ return {
32
+ type: "element",
33
+ tagName: TabItemTagname,
34
+ properties: {
35
+ dataLabel: title,
36
+ },
37
+ children: [node],
38
+ };
39
+ }
40
+ }
41
+
42
+ return node;
43
+ }
44
+
45
+ /**
46
+ * Processes the children of the `<CodeTabs>` component to transform
47
+ * any code blocks with "title" meta information into a <TabItem> component.
48
+ */
49
+ export function processCodeTabs(html: string): string {
50
+ const file = tabsProcessor.processSync({ value: html });
51
+ return file.toString();
52
+ }
@@ -0,0 +1,7 @@
1
+ export const PAGE_TITLE_ID = "_top";
2
+
3
+ // data-celestia-code-container
4
+ export const CODE_PROPERTY_CONTAINER = "dataCelestiaCodeContainer";
5
+
6
+ // data-celestia-code-title
7
+ export const CODE_PROPERTY_TITLE = "dataCelestiaCodeTitle";
@@ -0,0 +1,16 @@
1
+ import { docsLoader } from "@astrojs/starlight/loaders";
2
+ import { docsSchema } from "@astrojs/starlight/schema";
3
+ import { defineCollection } from "astro:content";
4
+ import type { CollectionConfig } from "astro/content/config";
5
+ import type { Loader } from "astro/loaders";
6
+
7
+ type ExtendedSchema = ReturnType<ReturnType<typeof docsSchema>>;
8
+
9
+ type Docs = CollectionConfig<ExtendedSchema, Loader>;
10
+
11
+ const docs: Docs = defineCollection<ExtendedSchema, Loader>({
12
+ loader: docsLoader(),
13
+ schema: docsSchema(),
14
+ });
15
+
16
+ export const collections = { docs };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This file contains some types by simplifying and modifying the generated
3
+ * files `.astro/content.d.ts`.
4
+ *
5
+ * Normally, Astro will generate a `.astro/content.d.ts` file for an Astro app
6
+ * project. However, since starlight-theme-celestia is a library instead of an app,
7
+ * we don't have those auto-generated files.
8
+ */
9
+
10
+ declare module "astro:content" {
11
+ export interface RenderResult {
12
+ Content: import("astro/runtime/server/index.js").AstroComponentFactory;
13
+ }
14
+ }
15
+
16
+ type ContentConfig = typeof import("./content.config.js");
17
+
18
+ type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
19
+
20
+ export type StarlightRouteEntryData = import("astro/zod").infer<
21
+ ReturnTypeOrOriginal<Required<ContentConfig["collections"]["docs"]>["schema"]>
22
+ >;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m12 19l-7-7l7-7m7 7H5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7-7l7 7l-7 7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 6L9 17l-5-5"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 18l-6-6l6-6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18l6-6l-6-6"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><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"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5h16M4 12h16M4 19h16"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="m21 21l-4.34-4.34"/><circle cx="11" cy="11" r="8"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Lucide by Lucide Contributors - https://github.com/lucide-icons/lucide/blob/main/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 1.992a10 10 0 1 0 9.236 13.838c.341-.82-.476-1.644-1.298-1.31a6.5 6.5 0 0 1-6.864-10.787l.077-.08c.551-.63.113-1.653-.758-1.653h-.266l-.068-.006z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><!-- Icon from Tabler Icons by Paweł Kuna - https://github.com/tabler/tabler-icons/blob/master/LICENSE --><path fill="currentColor" d="M12 19a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0v-2a1 1 0 0 1 1-1m-4.95-2.05a1 1 0 0 1 0 1.414l-1.414 1.414a1 1 0 1 1-1.414-1.414l1.414-1.414a1 1 0 0 1 1.414 0m11.314 0l1.414 1.414a1 1 0 0 1-1.414 1.414l-1.414-1.414a1 1 0 0 1 1.414-1.414m-5.049-9.836a5 5 0 1 1-2.532 9.674a5 5 0 0 1 2.532-9.674M4 11a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2zm18 0a1 1 0 0 1 0 2h-2a1 1 0 0 1 0-2zM5.636 4.222L7.05 5.636A1 1 0 0 1 5.636 7.05L4.222 5.636a1 1 0 0 1 1.414-1.414m14.142 0a1 1 0 0 1 0 1.414L18.364 7.05a1 1 0 0 1-1.414-1.414l1.414-1.414a1 1 0 0 1 1.414 0M12 1a1 1 0 0 1 1 1v2a1 1 0 0 1-2 0V2a1 1 0 0 1 1-1"/></svg>
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ import type { StarlightConfig, StarlightPlugin, StarlightUserConfig } from "@astrojs/starlight/types";
2
+ import type { AstroConfig } from "astro";
3
+ import remarkCustomHeaderId from "remark-custom-header-id";
4
+
5
+ import { createShikiConfig } from "./shiki-config";
6
+ import type { ThemeCelestiaOptions } from "./user-options";
7
+ import { vitePluginUserConfig } from "./virtual-user-config";
8
+
9
+ const components = {
10
+ Header: "starlight-theme-celestia/components/Header.astro",
11
+ Search: "starlight-theme-celestia/components/Search.astro",
12
+ ThemeProvider: "starlight-theme-celestia/components/ThemeProvider.astro",
13
+ ThemeSelect: "starlight-theme-celestia/components/ThemeSelect.astro",
14
+ SocialIcons: "starlight-theme-celestia/components/SocialIcons.astro",
15
+ SiteTitle: "starlight-theme-celestia/components/SiteTitleVersioned.astro",
16
+ PageFrame: "starlight-theme-celestia/components/PageFrame.astro",
17
+ Pagination: "starlight-theme-celestia/components/Pagination.astro",
18
+ MobileMenuToggle: "starlight-theme-celestia/components/MobileMenuToggle.astro",
19
+ TwoColumnContent: "starlight-theme-celestia/components/TwoColumnContent.astro",
20
+ MarkdownContent: "starlight-theme-celestia/components/MarkdownContent.astro",
21
+ Hero: "starlight-theme-celestia/components/Hero.astro",
22
+ MobileTableOfContents: "starlight-theme-celestia/components/MobileTableOfContents.astro",
23
+ MobileMenuFooter: "starlight-theme-celestia/components/MobileMenuFooter.astro",
24
+ LanguageSelect: "starlight-theme-celestia/components/LanguageSelect.astro",
25
+ Head: "starlight-theme-celestia/components/Head.astro",
26
+ } as const satisfies Partial<StarlightConfig["components"]>;
27
+
28
+ export type { ThemeCelestiaOptions };
29
+
30
+ export default function starlightCelestiaTheme(options: ThemeCelestiaOptions = {}): StarlightPlugin {
31
+ return {
32
+ name: "starlight-theme-celestia",
33
+ hooks: {
34
+ setup: async ({ config, updateConfig, addIntegration, astroConfig }) => {
35
+ let useTailwind: boolean;
36
+
37
+ if (options.stylingSystem === "css") {
38
+ useTailwind = false;
39
+ } else if (options.stylingSystem === "tailwind") {
40
+ useTailwind = true;
41
+ } else {
42
+ const hasTailwindcss = await checkHasPlugin(astroConfig.vite?.plugins, "tailwind");
43
+ useTailwind = hasTailwindcss;
44
+ }
45
+
46
+ const newConfig = {
47
+ customCss: [
48
+ "starlight-theme-celestia/layer.css",
49
+
50
+ useTailwind ? "" : "starlight-theme-celestia/tailwind.gen.css",
51
+
52
+ "starlight-theme-celestia/theme.css",
53
+
54
+ ...(config.customCss || []),
55
+
56
+ "starlight-theme-celestia/styles.css",
57
+ ].filter(Boolean),
58
+ components: {
59
+ ...components,
60
+ ...config.components,
61
+ },
62
+ expressiveCode: config.expressiveCode ?? false,
63
+ } satisfies Partial<StarlightUserConfig>;
64
+
65
+ updateConfig(newConfig);
66
+
67
+ addIntegration({
68
+ name: "starlight-theme-celestia-integration",
69
+ hooks: {
70
+ "astro:config:setup": ({ updateConfig }) => {
71
+ updateConfig({
72
+ markdown: {
73
+ shikiConfig: createShikiConfig({ twoslash: true }),
74
+ remarkPlugins: [remarkCustomHeaderId],
75
+ },
76
+ vite: {
77
+ plugins: [
78
+ vitePluginUserConfig({
79
+ nav: options.nav,
80
+ rootHref: astroConfig.root.toString(),
81
+ }),
82
+ ],
83
+ },
84
+ });
85
+ },
86
+ },
87
+ });
88
+ },
89
+ },
90
+ };
91
+ }
92
+
93
+ type ViteUserConfig = AstroConfig["vite"];
94
+ type VitePlugin = NonNullable<ViteUserConfig["plugins"]>[number];
95
+
96
+ async function checkHasPlugin(plugin: VitePlugin | Promise<VitePlugin>, search: string): Promise<boolean> {
97
+ const awaited = await plugin;
98
+
99
+ if (!awaited) {
100
+ return false;
101
+ }
102
+ if (Array.isArray(awaited)) {
103
+ for (const p of awaited) {
104
+ if (await checkHasPlugin(p, search)) {
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+ const name = awaited.name || "";
111
+ return name.includes(search);
112
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ transformerMetaHighlight,
3
+ transformerMetaWordHighlight,
4
+ transformerNotationDiff,
5
+ transformerRemoveNotationEscape,
6
+ transformerNotationHighlight,
7
+ transformerNotationWordHighlight,
8
+ } from "@shikijs/transformers";
9
+ import { transformerTwoslash } from "@shikijs/twoslash";
10
+ import type { ShikiTransformer } from "@shikijs/types";
11
+ import type { ShikiConfig } from "astro";
12
+ import { createRenderer } from "shiki-twoslash-renderer";
13
+
14
+ import { transformerContainer } from "./shiki-transformer-container";
15
+
16
+ export function createShikiConfig(options: { twoslash: boolean }): ShikiConfig {
17
+ const transformers: ShikiTransformer[] = [
18
+ transformerMetaHighlight(),
19
+ transformerMetaWordHighlight(),
20
+ transformerNotationDiff(),
21
+ transformerNotationHighlight(),
22
+ transformerNotationWordHighlight(),
23
+ transformerRemoveNotationEscape(),
24
+
25
+ transformerContainer(),
26
+ ...(options.twoslash
27
+ ? [
28
+ transformerTwoslash({
29
+ renderer: createRenderer(),
30
+ explicitTrigger: true,
31
+ twoslashOptions: {
32
+ compilerOptions: {
33
+ noUncheckedSideEffectImports: false,
34
+ },
35
+ },
36
+ }),
37
+ ]
38
+ : []),
39
+ ];
40
+
41
+ return {
42
+ themes: {
43
+ light: "one-light",
44
+ dark: "github-dark-dimmed",
45
+ },
46
+ defaultColor: false,
47
+ transformers: transformers,
48
+ };
49
+ }
@@ -0,0 +1,6 @@
1
+ export function parseTitleString(meta: string): string | null {
2
+ if (!meta) return null;
3
+ const match = meta.match(/title="([^"]+)"/);
4
+ if (!match) return null;
5
+ return match[1];
6
+ }
@@ -0,0 +1,85 @@
1
+ import type { ShikiTransformer } from "@shikijs/types";
2
+ import type { ElementContent, RootContent } from "hast";
3
+ import { isElement } from "hast-util-is-element";
4
+
5
+ import { CODE_PROPERTY_CONTAINER, CODE_PROPERTY_TITLE } from "./constants";
6
+ import { parseTitleString } from "./shiki-meta-title";
7
+
8
+ const name = "starlight-theme-celestia-shiki-transformer-container";
9
+
10
+ /**
11
+ * A transformer to wrap code blocks in a container.
12
+ */
13
+ export function transformerContainer(): ShikiTransformer {
14
+ return {
15
+ name: name,
16
+ root(node) {
17
+ if (node.children.length === 0) {
18
+ throw new Error(`[${name}] Expected at least one child`);
19
+ }
20
+
21
+ const pre = node.children.find((child) => isElement(child, "pre"));
22
+
23
+ if (!pre) {
24
+ throw new Error(`[${name}] Expected a <pre> element in the root node but got ${JSON.stringify(node)}`);
25
+ }
26
+
27
+ const title = parseTitleString(this.options.meta?.__raw || "");
28
+
29
+ const children: ElementContent[] = normalizeContent(node.children);
30
+
31
+ if (title) {
32
+ children.unshift({
33
+ type: "element",
34
+ tagName: "div",
35
+ properties: {
36
+ class:
37
+ "hidden border-b border-solid border-b-(--sl-color-gray-5) bg-(--sl-color-bg) px-4 py-2 font-mono text-sm text-(--sl-color-gray-3) [div[data-celestia-code-container][data-celestia-code-title]_&]:block",
38
+ },
39
+ children: [
40
+ {
41
+ type: "text",
42
+ value: title,
43
+ },
44
+ ],
45
+ });
46
+ }
47
+
48
+ node.children = [
49
+ {
50
+ type: "element",
51
+ tagName: "div",
52
+ properties: {
53
+ [CODE_PROPERTY_CONTAINER]: "",
54
+ [CODE_PROPERTY_TITLE]: title || undefined,
55
+ class:
56
+ "not-content relative overflow-hidden [&_pre]:relative [&_celestia-code-copy-button]:opacity-0 [&:hover_celestia-code-copy-button]:opacity-100 border border-solid border-(--sl-color-gray-5) rounded-md",
57
+ },
58
+ children: [
59
+ ...children,
60
+ {
61
+ type: "element",
62
+ tagName: "celestia-code-copy-button",
63
+ properties: {
64
+ type: "button",
65
+ "data-code": this.source,
66
+ title: "Copy code",
67
+ "aria-label": "Copy code",
68
+ class:
69
+ "absolute top-2 right-2 m-0 size-6 rounded border border-solid border-(--sl-color-gray-5) bg-gray-100/30 p-1 text-black backdrop-blur-sm transition hover:bg-gray-200/50 active:scale-90 dark:bg-gray-600/30 dark:text-white hover:dark:bg-gray-500/50",
70
+ },
71
+ children: [],
72
+ },
73
+ ],
74
+ },
75
+ ];
76
+
77
+ return node;
78
+ },
79
+ };
80
+ }
81
+
82
+ // A simple function to make typescript happy
83
+ function normalizeContent(children: RootContent[]): ElementContent[] {
84
+ return children.filter((child) => child.type !== "doctype");
85
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import "astro/client";
2
+ import "@astrojs/starlight";
@@ -0,0 +1,37 @@
1
+ export interface NavItem {
2
+ /**
3
+ * The visible label for this item in the navigation. It could be a string or
4
+ * a record of strings with the language code as the key.
5
+ */
6
+ label: string | Record<string, string>;
7
+ /**
8
+ * The link to this item's content. It could be a string or a record of
9
+ * strings with the language code as the key.
10
+ */
11
+ href: string | Record<string, string>;
12
+ }
13
+
14
+ export interface ThemeCelestiaOptions {
15
+ nav?: NavItem[];
16
+
17
+ /**
18
+ * `starlight-theme-celestia` supports two styling systems: pure CSS and Tailwind
19
+ * CSS.
20
+ *
21
+ * By default, `starlight-theme-celestia` will detect if Tailwind CSS is passed to
22
+ * the Astro config, and use Tailwind CSS if it is. You can also force the
23
+ * theme to use a specific styling system by setting the `stylingSystem`
24
+ * option to `css` or `tailwind`.
25
+ *
26
+ * @default "detect"
27
+ */
28
+ stylingSystem?: "css" | "tailwind" | "detect";
29
+ }
30
+
31
+ /**
32
+ * @internal
33
+ */
34
+ export interface ConfigSerialized {
35
+ nav?: NavItem[];
36
+ rootHref: string;
37
+ }
@@ -0,0 +1,44 @@
1
+ // Copied from https://github.com/withastro/starlight/blob/9d3ba179c5d524c1c61d771ceb1a7b4e754bee16/packages/starlight/virtual-internal.d.ts#L25
2
+ declare module "virtual:starlight/pagefind-config" {
3
+ import type { StarlightConfig } from "@astrojs/starlight/types";
4
+
5
+ export const pagefindUserConfig: Partial<Extract<StarlightConfig["pagefind"], object>>;
6
+ }
7
+
8
+ declare module "virtual:starlight-theme-celestia/user-config" {
9
+ const config: import("./user-options").ConfigSerialized;
10
+ export default config;
11
+ }
12
+ declare module "virtual:starlight-versions-config" {
13
+ export interface ConfigSerialized {
14
+ current: { label: string; redirect?: "same-page" | "root" };
15
+ versions: Array<{ slug: string; label?: string; redirect?: "same-page" | "root" }>;
16
+ versionsBySlug: Record<string, unknown>;
17
+ }
18
+ const config: ConfigSerialized;
19
+ export default config;
20
+ }
21
+ declare module "virtual:starlight/components/Search" {
22
+ const Search: typeof import("./components/Search.astro").default;
23
+ export default Search;
24
+ }
25
+ declare module "virtual:starlight/components/SiteTitle" {
26
+ const SiteTitle: typeof import("./components/SiteTitle.astro").default;
27
+ export default SiteTitle;
28
+ }
29
+ declare module "virtual:starlight/components/SocialIcons" {
30
+ const SocialIcons: typeof import("./components/SocialIcons.astro").default;
31
+ export default SocialIcons;
32
+ }
33
+ declare module "virtual:starlight/components/ThemeSelect" {
34
+ const ThemeSelect: typeof import("./components/ThemeSelect.astro").default;
35
+ export default ThemeSelect;
36
+ }
37
+ declare module "virtual:starlight/components/LanguageSelect" {
38
+ const LanguageSelect: typeof import("./components/LanguageSelect.astro").default;
39
+ export default LanguageSelect;
40
+ }
41
+ declare module "virtual:starlight/components/MobileMenuFooter" {
42
+ const MobileMenuFooter: typeof import("./components/MobileMenuFooter.astro").default;
43
+ export default MobileMenuFooter;
44
+ }
@@ -0,0 +1,44 @@
1
+ import type { ViteUserConfig } from "astro";
2
+
3
+ import type { ConfigSerialized } from "./user-options";
4
+
5
+ function resolveVirtualModuleId<T extends string>(id: T): `\0${T}` {
6
+ return `\0${id}`;
7
+ }
8
+
9
+ const VERSIONS_VIRTUAL_ID = "virtual:starlight-versions-config";
10
+ const VERSIONS_FALLBACK = `export default ${JSON.stringify({
11
+ current: { label: "latest", redirect: "root" },
12
+ versions: [],
13
+ versionsBySlug: {},
14
+ })}`;
15
+
16
+ /** Vite plugin that exposes user config via virtual modules. */
17
+ export function vitePluginUserConfig(config: ConfigSerialized): NonNullable<ViteUserConfig["plugins"]>[number] {
18
+ /** Map of virtual module names to their code contents as strings. */
19
+ const modules = {
20
+ "virtual:starlight-theme-celestia/user-config": `export default ${JSON.stringify(config)}`,
21
+ } satisfies Record<string, string>;
22
+
23
+ /** Mapping names prefixed with `\0` to their original form. */
24
+ const resolutionMap = Object.fromEntries(
25
+ (Object.keys(modules) as (keyof typeof modules)[]).map((key) => [resolveVirtualModuleId(key), key]),
26
+ );
27
+
28
+ return {
29
+ name: "vite-plugin-starlight-theme-celestia-user-config",
30
+ // enforce 'post' so starlight-versions' real config takes precedence over our fallback
31
+ enforce: "post" as const,
32
+ resolveId(id): string | void {
33
+ if (id in modules) return resolveVirtualModuleId(id);
34
+ // Fallback for virtual:starlight-versions-config when the plugin isn't configured
35
+ if (id === VERSIONS_VIRTUAL_ID) return resolveVirtualModuleId(id);
36
+ },
37
+ load(id): string | void {
38
+ const resolution = resolutionMap[id];
39
+ if (resolution) return modules[resolution];
40
+ // Provide empty fallback if starlight-versions plugin didn't load
41
+ if (id === resolveVirtualModuleId(VERSIONS_VIRTUAL_ID)) return VERSIONS_FALLBACK;
42
+ },
43
+ };
44
+ }