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,62 @@
1
+ ---
2
+ import config from "virtual:starlight/user-config";
3
+ import VersionSelect from "./VersionSelect.astro";
4
+
5
+ const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute;
6
+
7
+ // Resolve the logo src to a public path.
8
+ // Starlight's default SiteTitle uses virtual:starlight/user-images which goes through
9
+ // Astro's image pipeline, causing re-processing on each page load and logo flicker.
10
+ // We render the logo as a plain <img> from the public directory instead.
11
+ let logoSrc: string | undefined;
12
+ let logoAlt = "";
13
+ if (config.logo) {
14
+ logoAlt = config.logo.alt || "";
15
+ if ("src" in config.logo) {
16
+ // Single logo — resolve the public path from the src string
17
+ const raw = typeof config.logo.src === "string" ? config.logo.src : "";
18
+ logoSrc = raw.replace(/^\.\/public/, "");
19
+ }
20
+ }
21
+ ---
22
+
23
+ <div class="site-title-wrapper">
24
+ <a href={siteTitleHref} class="site-title sl-flex">
25
+ {logoSrc && <img alt={logoAlt} src={logoSrc} class="celestia-logo" />}
26
+ <span class:list={{ "sr-only": config.logo?.replacesTitle }} translate="no">
27
+ {siteTitle}
28
+ </span>
29
+ </a>
30
+ <VersionSelect />
31
+ </div>
32
+
33
+ <style>
34
+ .site-title-wrapper {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 1.25rem;
38
+ }
39
+
40
+ .site-title {
41
+ align-items: center;
42
+ gap: var(--sl-nav-gap);
43
+ font-size: var(--sl-text-h4);
44
+ font-weight: 600;
45
+ color: var(--sl-color-text-accent);
46
+ text-decoration: none;
47
+ white-space: nowrap;
48
+ min-width: 0;
49
+ }
50
+
51
+ .celestia-logo {
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
+ span {
60
+ overflow: hidden;
61
+ }
62
+ </style>
@@ -0,0 +1,31 @@
1
+ ---
2
+ import { Icon } from '@astrojs/starlight/components'
3
+ import config from 'virtual:starlight/user-config'
4
+
5
+ const links = config.social || []
6
+
7
+ if (!Array.isArray(links)) {
8
+ const message =
9
+ '[starlight-theme-celestia] ' +
10
+ 'Starlight v0.33.0 changed the `social` configuration syntax. Please specify an array of link items instead of an object.\n' +
11
+ 'See the Starlight changelog for details: https://github.com/withastro/starlight/blob/cf3cec1b/packages/starlight/CHANGELOG.md#0330\n'
12
+ throw new Error(message)
13
+ }
14
+ ---
15
+
16
+ {
17
+ links.length > 0 && (
18
+ <>
19
+ {links.map(({ label, href, icon }) => (
20
+ <a
21
+ href={href}
22
+ rel="me"
23
+ class="flex size-8 rounded-md p-2 text-(--sl-color-text) transition hover:bg-gray-400/30 focus-visible:outline-offset-1 active:scale-90"
24
+ >
25
+ <span class="sr-only">{label}</span>
26
+ <Icon name={icon} size="1rem" color="currentColor" />
27
+ </a>
28
+ ))}
29
+ </>
30
+ )
31
+ }
@@ -0,0 +1,102 @@
1
+ {/* This is intentionally inlined to avoid FOUC. */}
2
+ <script is:inline>
3
+ // Sidebar/tab override CSS that must persist across View Transitions.
4
+ var celestiaOverrideCSS = `
5
+ .sidebar-content a {
6
+ transition: none !important;
7
+ background-color: transparent !important;
8
+ background: none !important;
9
+ }
10
+ .sidebar-content a[aria-current="page"] {
11
+ color: var(--sl-color-white) !important;
12
+ border-left: 2px solid var(--sl-color-accent-high) !important;
13
+ border-radius: 0 !important;
14
+ padding-left: 0.5em !important;
15
+ }
16
+ [role="tablist"] [role="tab"] {
17
+ border: none !important;
18
+ box-shadow: inset 0 -1px 0 var(--sl-color-gray-5) !important;
19
+ }
20
+ [role="tablist"] [role="tab"][aria-selected="true"] {
21
+ box-shadow: inset 0 -2px 0 var(--sl-color-accent-high) !important;
22
+ }
23
+ `;
24
+
25
+ function ensureCelestiaOverrides() {
26
+ if (!document.getElementById('celestia-sidebar-overrides')) {
27
+ var s = document.createElement('style');
28
+ s.id = 'celestia-sidebar-overrides';
29
+ s.textContent = celestiaOverrideCSS;
30
+ document.head.prepend(s);
31
+ }
32
+ }
33
+
34
+ // Inject immediately
35
+ ensureCelestiaOverrides();
36
+
37
+ // Preserve during View Transition swaps: copy the style element into the
38
+ // new document before Astro replaces the current one.
39
+ document.addEventListener('astro:before-swap', function(e) {
40
+ var existing = document.getElementById('celestia-sidebar-overrides');
41
+ if (existing && e.newDocument) {
42
+ e.newDocument.head.prepend(existing.cloneNode(true));
43
+ }
44
+ });
45
+
46
+ // Fallback: re-inject after swap in case the above didn't fire
47
+ document.addEventListener('astro:after-swap', ensureCelestiaOverrides);
48
+
49
+ window.StarlightThemeProvider = (() => {
50
+ const storedTheme =
51
+ typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
52
+ const theme =
53
+ storedTheme ||
54
+ (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
55
+
56
+ // Set theme immediately before any styles load
57
+ document.documentElement.dataset.theme = theme === 'light' ? 'light' : 'dark';
58
+ document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark';
59
+
60
+ // Add class to prevent transitions during initial load
61
+ document.documentElement.classList.add('loading');
62
+
63
+ // Disable transitions temporarily to prevent color flash on load
64
+ const style = document.createElement('style');
65
+ style.textContent = `
66
+ .loading *, .loading *::before, .loading *::after {
67
+ transition: none !important;
68
+ animation-duration: 0s !important;
69
+ }
70
+ `;
71
+ document.head.appendChild(style);
72
+
73
+ // Re-enable transitions after page is fully loaded
74
+ const enableTransitions = () => {
75
+ requestAnimationFrame(() => {
76
+ requestAnimationFrame(() => {
77
+ document.documentElement.classList.remove('loading');
78
+ style.remove();
79
+ });
80
+ });
81
+ };
82
+
83
+ if (document.readyState === 'loading') {
84
+ document.addEventListener('DOMContentLoaded', enableTransitions);
85
+ } else {
86
+ enableTransitions();
87
+ }
88
+
89
+ return {
90
+ updatePickers(theme) {
91
+ if (theme === undefined) {
92
+ var stored = typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
93
+ theme = stored || 'auto';
94
+ }
95
+ document.querySelectorAll('starlight-theme-select').forEach((picker) => {
96
+ const select = picker.querySelector('select');
97
+ if (select) select.value = theme;
98
+ });
99
+ },
100
+ };
101
+ })();
102
+ </script>
@@ -0,0 +1,188 @@
1
+ ---
2
+ // Custom theme select component that cycles through auto/light/dark themes
3
+ // Note: innerHTML usage below is safe - values are hardcoded SVG constants, not user input
4
+ ---
5
+
6
+ <starlight-theme-select>
7
+ <button
8
+ type="button"
9
+ class="theme-toggle"
10
+ aria-label="Toggle theme"
11
+ data-theme="auto"
12
+ >
13
+ <svg class="theme-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
14
+ <path d="M4 6V16H20V6H4ZM20 18H4C2.89 18 2 17.11 2 16V6C2 4.89 2.89 4 4 4H20C21.11 4 22 4.89 22 6V16C22 17.11 21.11 18 20 18ZM23 20H1C0.45 20 0 20.45 0 21S0.45 22 1 22H23C23.55 22 24 21.55 24 21S23.55 20 23 20Z"/>
15
+ </svg>
16
+ </button>
17
+ </starlight-theme-select>
18
+
19
+ {/* Initialize button with correct theme immediately to avoid icon flash */}
20
+ <script is:inline>
21
+ (function() {
22
+ const storedTheme = typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
23
+ const theme = storedTheme || 'auto';
24
+
25
+ // Safe: These are hardcoded SVG paths, not user input
26
+ const themeIcons = {
27
+ auto: '<path d="M4 6V16H20V6H4ZM20 18H4C2.89 18 2 17.11 2 16V6C2 4.89 2.89 4 4 4H20C21.11 4 22 4.89 22 6V16C22 17.11 21.11 18 20 18ZM23 20H1C0.45 20 0 20.45 0 21S0.45 22 1 22H23C23.55 22 24 21.55 24 21S23.55 20 23 20Z"/>',
28
+ light: '<path d="M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31L23.31,12L20,8.69Z"/>',
29
+ dark: '<path d="M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.4 6.35,17.41C9.37,20.43 14,20.54 17.33,17.97Z"/>'
30
+ };
31
+
32
+ var themeLabels = { auto: 'Auto', light: 'Light', dark: 'Dark' };
33
+
34
+ var buttons = document.querySelectorAll('starlight-theme-select button');
35
+ buttons.forEach(function(button) {
36
+ button.dataset.theme = theme;
37
+ var icon = button.querySelector('.theme-icon');
38
+ if (icon) {
39
+ var svgContent = themeIcons[theme];
40
+ while (icon.firstChild) icon.removeChild(icon.firstChild);
41
+ var temp = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
42
+ temp.innerHTML = svgContent;
43
+ while (temp.firstChild) icon.appendChild(temp.firstChild);
44
+ }
45
+ button.setAttribute('aria-label', 'Theme: ' + themeLabels[theme]);
46
+ button.style.visibility = 'visible';
47
+ });
48
+ })();
49
+ </script>
50
+
51
+ <style>
52
+ .theme-toggle {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ padding: 0.4em;
57
+ background: transparent;
58
+ border: none;
59
+ color: var(--sl-color-text-accent);
60
+ cursor: pointer;
61
+ }
62
+
63
+ .theme-toggle:hover {
64
+ opacity: 0.66;
65
+ }
66
+
67
+ .theme-icon {
68
+ flex-shrink: 0;
69
+ width: 1.125rem;
70
+ height: 1.125rem;
71
+ }
72
+ </style>
73
+
74
+ {/* Inlined to avoid FOUC. Uses global scope from `ThemeProvider.astro` */}
75
+ <script is:inline>
76
+ if (typeof StarlightThemeProvider !== 'undefined' && StarlightThemeProvider.updatePickers) {
77
+ StarlightThemeProvider.updatePickers();
78
+ }
79
+ </script>
80
+
81
+ <script>
82
+ type Theme = 'auto' | 'dark' | 'light';
83
+
84
+ /** Key in `localStorage` to store color theme preference at. */
85
+ const storageKey = 'starlight-theme';
86
+
87
+ /** Theme cycle order */
88
+ const themes: Theme[] = ['auto', 'light', 'dark'];
89
+
90
+ // Safe: These are hardcoded SVG paths, not user input
91
+ const themeIcons: Record<Theme, string> = {
92
+ auto: '<path d="M4 6V16H20V6H4ZM20 18H4C2.89 18 2 17.11 2 16V6C2 4.89 2.89 4 4 4H20C21.11 4 22 4.89 22 6V16C22 17.11 21.11 18 20 18ZM23 20H1C0.45 20 0 20.45 0 21S0.45 22 1 22H23C23.55 22 24 21.55 24 21S23.55 20 23 20Z"/>',
93
+ light: '<path d="M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8M12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18M20,8.69V4H15.31L12,0.69L8.69,4H4V8.69L0.69,12L4,15.31V20H8.69L12,23.31L15.31,20H20V15.31L23.31,12L20,8.69Z"/>',
94
+ dark: '<path d="M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.4 6.35,17.41C9.37,20.43 14,20.54 17.33,17.97Z"/>'
95
+ };
96
+
97
+ /** Theme labels for display */
98
+ const themeLabels: Record<Theme, string> = {
99
+ auto: 'Auto',
100
+ light: 'Light',
101
+ dark: 'Dark'
102
+ };
103
+
104
+ /** Get a typesafe theme string from any JS value (unknown values are coerced to `'auto'`). */
105
+ const parseTheme = (theme: unknown): Theme =>
106
+ theme === 'auto' || theme === 'dark' || theme === 'light' ? theme : 'auto';
107
+
108
+ /** Load the user's preference from `localStorage`. */
109
+ const loadTheme = (): Theme =>
110
+ parseTheme(typeof localStorage !== 'undefined' && localStorage.getItem(storageKey));
111
+
112
+ /** Store the user's preference in `localStorage`. */
113
+ function storeTheme(theme: Theme): void {
114
+ if (typeof localStorage !== 'undefined') {
115
+ localStorage.setItem(storageKey, theme === 'light' || theme === 'dark' ? theme : '');
116
+ }
117
+ }
118
+
119
+ /** Get the preferred system color scheme. */
120
+ const getPreferredColorScheme = (): Theme =>
121
+ matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
122
+
123
+ /** Get the next theme in the cycle */
124
+ function getNextTheme(currentTheme: Theme): Theme {
125
+ const currentIndex = themes.indexOf(currentTheme);
126
+ const nextIndex = (currentIndex + 1) % themes.length;
127
+ return themes[nextIndex];
128
+ }
129
+
130
+ /** Helper to set SVG content safely using DOM methods */
131
+ function setSvgContent(svgElement: Element, svgMarkup: string): void {
132
+ while (svgElement.firstChild) svgElement.removeChild(svgElement.firstChild);
133
+ const temp = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
134
+ temp.innerHTML = svgMarkup;
135
+ while (temp.firstChild) svgElement.appendChild(temp.firstChild);
136
+ }
137
+
138
+ /** Update button UI, document theme, and local storage state. */
139
+ function onThemeChange(theme: Theme): void {
140
+ document.querySelectorAll('starlight-theme-select button').forEach((button) => {
141
+ if (button instanceof HTMLButtonElement) {
142
+ button.dataset.theme = theme;
143
+
144
+ const icon = button.querySelector('.theme-icon');
145
+ if (icon) {
146
+ setSvgContent(icon, themeIcons[theme]);
147
+ }
148
+
149
+ button.setAttribute('aria-label', `Theme: ${themeLabels[theme]}`);
150
+ }
151
+ });
152
+
153
+ const resolvedTheme = theme === 'auto' ? getPreferredColorScheme() : theme;
154
+ document.documentElement.dataset.theme = resolvedTheme;
155
+ document.documentElement.style.colorScheme = resolvedTheme;
156
+
157
+ storeTheme(theme);
158
+
159
+ if (typeof (globalThis as any).StarlightThemeProvider !== 'undefined' && (globalThis as any).StarlightThemeProvider.updatePickers) {
160
+ (globalThis as any).StarlightThemeProvider.updatePickers(theme);
161
+ }
162
+ }
163
+
164
+ // React to changes in system color scheme.
165
+ matchMedia(`(prefers-color-scheme: light)`).addEventListener('change', () => {
166
+ if (loadTheme() === 'auto') onThemeChange('auto');
167
+ });
168
+
169
+ class StarlightThemeSelect extends HTMLElement {
170
+ constructor() {
171
+ super();
172
+
173
+ const currentTheme = loadTheme();
174
+ onThemeChange(currentTheme);
175
+
176
+ const button = this.querySelector('button');
177
+ if (button) {
178
+ button.addEventListener('click', () => {
179
+ const currentTheme = parseTheme(button.dataset.theme);
180
+ const nextTheme = getNextTheme(currentTheme);
181
+ onThemeChange(nextTheme);
182
+ });
183
+ }
184
+ }
185
+ }
186
+
187
+ customElements.define('starlight-theme-select', StarlightThemeSelect);
188
+ </script>
@@ -0,0 +1,44 @@
1
+ <div class="lg:sl-flex">
2
+ {
3
+ Astro.locals.starlightRoute.toc && (
4
+ <aside class="right-sidebar-container print:hidden">
5
+ <div class="right-sidebar">
6
+ <slot name="right-sidebar" />
7
+ </div>
8
+ </aside>
9
+ )
10
+ }
11
+ <div class="main-pane"><slot /></div>
12
+ </div>
13
+
14
+ <style>
15
+ @layer celestia {
16
+ .main-pane {
17
+ isolation: isolate;
18
+ overflow-x: hidden;
19
+ }
20
+
21
+ @media (min-width: 72rem) {
22
+ .right-sidebar-container {
23
+ order: 2;
24
+ position: relative;
25
+ width: var(--sl-sidebar-width);
26
+ }
27
+
28
+ .right-sidebar {
29
+ position: fixed;
30
+ top: 0;
31
+ right: 0;
32
+ padding-top: var(--sl-nav-height);
33
+ height: 100vh;
34
+ width: var(--sl-sidebar-width);
35
+ overflow-y: auto;
36
+ scrollbar-width: none;
37
+ }
38
+
39
+ .main-pane {
40
+ flex: 1;
41
+ }
42
+ }
43
+ }
44
+ </style>
@@ -0,0 +1,261 @@
1
+ ---
2
+ /**
3
+ * Version dropdown selector for use with starlight-versions.
4
+ * This component is only rendered when starlight-versions is configured.
5
+ */
6
+ import { Icon } from "@astrojs/starlight/components";
7
+ import starlightConfig from "virtual:starlight/user-config";
8
+ import starlightVersionsConfig from "virtual:starlight-versions-config";
9
+
10
+ type Version = {
11
+ slug: string;
12
+ label?: string;
13
+ redirect?: "same-page" | "root";
14
+ };
15
+
16
+ function getVersionFromSlug(
17
+ config: typeof starlightVersionsConfig,
18
+ slug: string,
19
+ ): Version | undefined {
20
+ const segments = slug.split("/");
21
+ const versionOrLocaleSegment = segments[0];
22
+ if (!versionOrLocaleSegment) return undefined;
23
+ const version = config.versions.find(
24
+ (v: Version) => v.slug === versionOrLocaleSegment,
25
+ );
26
+ if (version) return version;
27
+ const locales = Object.keys(starlightConfig.locales ?? {});
28
+ if (!locales.includes(versionOrLocaleSegment)) return undefined;
29
+ const versionSegment = segments[1];
30
+ return config.versions.find((v: Version) => v.slug === versionSegment);
31
+ }
32
+
33
+ function stripTrailingSlash(path: string): string {
34
+ return path.endsWith("/") ? path.slice(0, -1) : path;
35
+ }
36
+
37
+ function getExtension(path: string): string {
38
+ const match = path.match(/\.[^.]+$/);
39
+ return match ? match[0] : "";
40
+ }
41
+
42
+ function stripExtension(path: string): string {
43
+ return path.replace(/\.[^.]+$/, "");
44
+ }
45
+
46
+ function getVersionURL(
47
+ config: typeof starlightVersionsConfig,
48
+ url: URL,
49
+ version: Version | undefined,
50
+ ): URL {
51
+ const versionURL = new URL(url);
52
+ const versionSlug = version?.slug ?? "";
53
+ const versionRedirect = version?.redirect ?? config.current.redirect;
54
+
55
+ const base = stripTrailingSlash(import.meta.env.BASE_URL);
56
+ const hasBase = versionURL.pathname.startsWith(base);
57
+
58
+ if (hasBase) {
59
+ versionURL.pathname = versionURL.pathname.replace(base, "");
60
+ }
61
+
62
+ let baseSegment: string | undefined;
63
+ let localeSegment: string | undefined;
64
+
65
+ const isHTML = getExtension(versionURL.pathname) === ".html";
66
+ const [, firstSegment, secondSegment] = versionURL.pathname.split("/");
67
+
68
+ if (starlightConfig.isMultilingual || starlightConfig.locales) {
69
+ const versionOrLocale = firstSegment?.replace(".html", "");
70
+ const isRootLocale =
71
+ versionOrLocale &&
72
+ !Object.keys(starlightConfig.locales ?? {}).includes(versionOrLocale);
73
+ baseSegment = isRootLocale ? firstSegment : secondSegment;
74
+
75
+ if (!isRootLocale) {
76
+ localeSegment = versionOrLocale;
77
+ versionURL.pathname = versionURL.pathname.replace(
78
+ `/${firstSegment}`,
79
+ "",
80
+ );
81
+ }
82
+ } else {
83
+ baseSegment = firstSegment;
84
+ }
85
+
86
+ const isRootHTML = baseSegment && getExtension(baseSegment) === ".html";
87
+ const baseSlug =
88
+ baseSegment && isRootHTML ? stripExtension(baseSegment) : baseSegment;
89
+
90
+ if (baseSlug && baseSlug in config.versionsBySlug) {
91
+ if (versionSlug) {
92
+ versionURL.pathname =
93
+ versionRedirect === "same-page"
94
+ ? versionURL.pathname.replace(baseSlug, versionSlug)
95
+ : `${versionSlug}${isHTML ? ".html" : "/"}`;
96
+ } else if (isRootHTML) {
97
+ versionURL.pathname = "/index.html";
98
+ } else {
99
+ versionURL.pathname =
100
+ versionRedirect === "same-page"
101
+ ? versionURL.pathname.replace(`/${baseSlug}`, "")
102
+ : isHTML
103
+ ? "/index.html"
104
+ : "/";
105
+ }
106
+ } else if (versionSlug) {
107
+ versionURL.pathname =
108
+ baseSegment === "index.html"
109
+ ? `/${versionSlug}.html`
110
+ : versionRedirect === "same-page"
111
+ ? `/${versionSlug}${versionURL.pathname}`
112
+ : isHTML
113
+ ? `${versionSlug}.html`
114
+ : `/${versionSlug}/`;
115
+ } else if (versionRedirect === "root" && !isRootHTML) {
116
+ versionURL.pathname = isHTML ? "/index.html" : versionSlug;
117
+ }
118
+
119
+ if (localeSegment) {
120
+ versionURL.pathname = isHTML
121
+ ? versionURL.pathname === "/index.html"
122
+ ? `/${localeSegment}.html`
123
+ : `/${localeSegment}${versionURL.pathname.replace(/\/$/, ".html")}`
124
+ : `/${localeSegment}${versionURL.pathname}`;
125
+ }
126
+
127
+ if (hasBase) {
128
+ versionURL.pathname = base + versionURL.pathname;
129
+ }
130
+
131
+ return versionURL;
132
+ }
133
+
134
+ const hasVersions = starlightVersionsConfig.versions.length > 0;
135
+ const { id } = Astro.locals.starlightRoute;
136
+ const pageVersion = hasVersions
137
+ ? getVersionFromSlug(starlightVersionsConfig, id)
138
+ : undefined;
139
+ ---
140
+
141
+ {hasVersions && <starlight-version-select>
142
+ <label>
143
+ <span class="sr-only">Select version</span>
144
+ <Icon name="open-book" class="icon label-icon" />
145
+ <select
146
+ value={pageVersion?.label ?? starlightVersionsConfig.versions[0]?.label}
147
+ autocomplete="off"
148
+ >
149
+ {
150
+ starlightVersionsConfig.versions.map((version: Version) => (
151
+ <option
152
+ selected={
153
+ version.slug === pageVersion?.slug ||
154
+ (pageVersion === undefined &&
155
+ version === starlightVersionsConfig.versions[0])
156
+ }
157
+ value={
158
+ getVersionURL(starlightVersionsConfig, Astro.url, version)
159
+ .pathname
160
+ }
161
+ >
162
+ {version.label ?? version.slug}
163
+ </option>
164
+ ))
165
+ }
166
+ </select>
167
+ <Icon name="down-caret" class="icon caret-icon" />
168
+ </label>
169
+ </starlight-version-select>}
170
+
171
+ <style>
172
+ label {
173
+ --sl-versions-label-icon-size: 0.75rem;
174
+ --sl-versions-caret-icon-size: 1rem;
175
+
176
+ align-items: center;
177
+ color: var(--sl-color-gray-1);
178
+ display: flex;
179
+ gap: 0.125rem;
180
+ position: relative;
181
+ font-size: 0.8rem;
182
+ }
183
+
184
+ label:hover {
185
+ color: var(--sl-color-gray-2);
186
+ }
187
+
188
+ .icon {
189
+ pointer-events: none;
190
+ position: absolute;
191
+ top: 50%;
192
+ transform: translateY(-50%);
193
+ }
194
+
195
+ .label-icon {
196
+ font-size: var(--sl-versions-label-icon-size);
197
+ inset-inline-start: 0;
198
+ }
199
+
200
+ .caret-icon {
201
+ font-size: var(--sl-versions-caret-icon-size);
202
+ inset-inline-end: 0;
203
+ }
204
+
205
+ select {
206
+ appearance: none;
207
+ background-color: transparent;
208
+ border: 0;
209
+ color: inherit;
210
+ cursor: pointer;
211
+ font-size: 0.8rem;
212
+ padding-block: 0.375rem;
213
+ padding-inline: calc(var(--sl-versions-label-icon-size) + 0.25rem)
214
+ calc(var(--sl-versions-caret-icon-size) + 0.125rem);
215
+ text-overflow: ellipsis;
216
+ width: 4.5rem;
217
+ }
218
+
219
+ option {
220
+ background-color: var(--sl-color-bg-nav);
221
+ color: var(--sl-color-gray-1);
222
+ }
223
+
224
+ @media (min-width: 50rem) {
225
+ select {
226
+ font-size: 0.8rem;
227
+ }
228
+ }
229
+ </style>
230
+
231
+ <script>
232
+ if (!customElements.get("starlight-version-select")) {
233
+ customElements.define(
234
+ "starlight-version-select",
235
+ class StarlightVersionSelect extends HTMLElement {
236
+ constructor() {
237
+ super();
238
+ const select = this.querySelector("select");
239
+ if (!select) return;
240
+
241
+ select.addEventListener("change", (event) => {
242
+ if (event.currentTarget instanceof HTMLSelectElement) {
243
+ globalThis.location.pathname = event.currentTarget.value;
244
+ }
245
+ });
246
+
247
+ window.addEventListener("pageshow", (event) => {
248
+ if (!event.persisted) return;
249
+ const markupSelectedIndex =
250
+ select.querySelector<HTMLOptionElement>(
251
+ "option[selected]",
252
+ )?.index;
253
+ if (markupSelectedIndex !== select.selectedIndex) {
254
+ select.selectedIndex = markupSelectedIndex ?? 0;
255
+ }
256
+ });
257
+ }
258
+ },
259
+ );
260
+ }
261
+ </script>