@astrojs/starlight 0.21.5 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/__e2e__/fixtures/basics/astro.config.mjs +10 -0
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +17 -0
- package/__e2e__/fixtures/basics/package.json +9 -0
- package/__e2e__/fixtures/basics/src/content/config.ts +6 -0
- package/__e2e__/fixtures/basics/src/content/docs/tabs-variable-height.mdx +151 -0
- package/__e2e__/fixtures/basics/src/content/docs/tabs.mdx +37 -0
- package/__e2e__/fixtures/basics/src/env.d.ts +2 -0
- package/__e2e__/tabs.test.ts +122 -0
- package/__e2e__/test-utils.ts +53 -0
- package/components/ContentNotice.astro +31 -0
- package/components/DraftContentNotice.astro +8 -0
- package/components/FallbackContentNotice.astro +2 -21
- package/components/Head.astro +3 -3
- package/components/Icons.ts +9 -0
- package/components/Page.astro +2 -0
- package/components/SiteTitle.astro +2 -1
- package/components/ThemeSelect.astro +36 -48
- package/global.d.ts +1 -1
- package/index.astro +1 -1
- package/integrations/expressive-code/hast.d.ts +5 -0
- package/integrations/expressive-code/hast.mjs +9 -0
- package/package.json +13 -3
- package/playwright.config.ts +15 -0
- package/schema.ts +16 -3
- package/schemas/components.ts +9 -0
- package/schemas/i18n.ts +18 -5
- package/schemas/site-title.ts +22 -0
- package/schemas/social.ts +2 -0
- package/style/props.css +1 -1
- package/translations/ar.json +1 -0
- package/translations/cs.json +1 -0
- package/translations/da.json +1 -0
- package/translations/de.json +1 -0
- package/translations/en.json +1 -0
- package/translations/es.json +1 -0
- package/translations/fa.json +8 -7
- package/translations/fr.json +1 -0
- package/translations/gl.json +1 -0
- package/translations/he.json +1 -0
- package/translations/hi.json +1 -0
- package/translations/id.json +1 -0
- package/translations/it.json +1 -0
- package/translations/ja.json +1 -0
- package/translations/ko.json +1 -0
- package/translations/nb.json +1 -0
- package/translations/nl.json +1 -0
- package/translations/pl.json +1 -0
- package/translations/pt.json +1 -0
- package/translations/ro.json +1 -0
- package/translations/ru.json +5 -1
- package/translations/sv.json +1 -0
- package/translations/tr.json +1 -0
- package/translations/uk.json +1 -0
- package/translations/vi.json +1 -0
- package/translations/zh-CN.json +1 -0
- package/translations/zh-TW.json +1 -0
- package/user-components/Tabs.astro +44 -3
- package/utils/createTranslationSystem.ts +10 -8
- package/utils/route-data.ts +14 -1
- package/utils/routing.ts +9 -6
- package/utils/starlight-page.ts +2 -1
- package/utils/translations.ts +6 -2
- package/utils/user-config.ts +21 -12
- package/virtual.d.ts +4 -0
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
import Icon from './Icon.astro';
|
|
3
3
|
import { processPanels } from './rehype-tabs';
|
|
4
4
|
|
|
5
|
+
interface Props {
|
|
6
|
+
syncKey?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { syncKey } = Astro.props;
|
|
5
10
|
const panelHtml = await Astro.slots.render('default');
|
|
6
11
|
const { html, panels } = processPanels(panelHtml);
|
|
7
12
|
---
|
|
8
13
|
|
|
9
|
-
<starlight-tabs>
|
|
14
|
+
<starlight-tabs data-sync-key={syncKey}>
|
|
10
15
|
{
|
|
11
16
|
panels && (
|
|
12
17
|
<div class="tablist-wrapper not-content">
|
|
@@ -74,14 +79,25 @@ const { html, panels } = processPanels(panelHtml);
|
|
|
74
79
|
|
|
75
80
|
<script>
|
|
76
81
|
class StarlightTabs extends HTMLElement {
|
|
82
|
+
// A map of sync keys to all tabs that are synced to that key.
|
|
83
|
+
static #syncedTabs = new Map<string, StarlightTabs[]>();
|
|
84
|
+
|
|
77
85
|
tabs: HTMLAnchorElement[];
|
|
78
86
|
panels: HTMLElement[];
|
|
87
|
+
#syncKey: string | undefined;
|
|
79
88
|
|
|
80
89
|
constructor() {
|
|
81
90
|
super();
|
|
82
91
|
const tablist = this.querySelector<HTMLUListElement>('[role="tablist"]')!;
|
|
83
92
|
this.tabs = [...tablist.querySelectorAll<HTMLAnchorElement>('[role="tab"]')];
|
|
84
93
|
this.panels = [...this.querySelectorAll<HTMLElement>(':scope > [role="tabpanel"]')];
|
|
94
|
+
this.#syncKey = this.dataset.syncKey;
|
|
95
|
+
|
|
96
|
+
if (this.#syncKey) {
|
|
97
|
+
const syncedTabs = StarlightTabs.#syncedTabs.get(this.#syncKey) ?? [];
|
|
98
|
+
syncedTabs.push(this);
|
|
99
|
+
StarlightTabs.#syncedTabs.set(this.#syncKey, syncedTabs);
|
|
100
|
+
}
|
|
85
101
|
|
|
86
102
|
this.tabs.forEach((tab, i) => {
|
|
87
103
|
// Handle clicks for mouse users
|
|
@@ -117,9 +133,14 @@ const { html, panels } = processPanels(panelHtml);
|
|
|
117
133
|
});
|
|
118
134
|
}
|
|
119
135
|
|
|
120
|
-
switchTab(newTab: HTMLAnchorElement | null | undefined, index: number) {
|
|
136
|
+
switchTab(newTab: HTMLAnchorElement | null | undefined, index: number, shouldSync = true) {
|
|
121
137
|
if (!newTab) return;
|
|
122
138
|
|
|
139
|
+
// If tabs should be synced, we store the current position so we can restore it after
|
|
140
|
+
// switching tabs to prevent the page from jumping when the new tab content is of a different
|
|
141
|
+
// height than the previous tab.
|
|
142
|
+
const previousTabsOffset = shouldSync ? this.getBoundingClientRect().top : 0;
|
|
143
|
+
|
|
123
144
|
// Mark all tabs as unselected and hide all tab panels.
|
|
124
145
|
this.tabs.forEach((tab) => {
|
|
125
146
|
tab.setAttribute('aria-selected', 'false');
|
|
@@ -135,7 +156,27 @@ const { html, panels } = processPanels(panelHtml);
|
|
|
135
156
|
// Restore active tab to the default tab order.
|
|
136
157
|
newTab.removeAttribute('tabindex');
|
|
137
158
|
newTab.setAttribute('aria-selected', 'true');
|
|
138
|
-
|
|
159
|
+
if (shouldSync) {
|
|
160
|
+
newTab.focus();
|
|
161
|
+
StarlightTabs.#syncTabs(this, newTab.textContent);
|
|
162
|
+
window.scrollTo({
|
|
163
|
+
top: window.scrollY + (this.getBoundingClientRect().top - previousTabsOffset),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static #syncTabs(emitter: StarlightTabs, label: string | null) {
|
|
169
|
+
const syncKey = emitter.#syncKey;
|
|
170
|
+
if (!syncKey || !label) return;
|
|
171
|
+
const syncedTabs = StarlightTabs.#syncedTabs.get(syncKey);
|
|
172
|
+
if (!syncedTabs) return;
|
|
173
|
+
|
|
174
|
+
for (const receiver of syncedTabs) {
|
|
175
|
+
if (receiver === emitter) continue;
|
|
176
|
+
const labelIndex = receiver.tabs.findIndex((tab) => tab.textContent === label);
|
|
177
|
+
if (labelIndex === -1) continue;
|
|
178
|
+
receiver.switchTab(receiver.tabs[labelIndex], labelIndex, false);
|
|
179
|
+
}
|
|
139
180
|
}
|
|
140
181
|
}
|
|
141
182
|
|
|
@@ -2,8 +2,8 @@ import type { i18nSchemaOutput } from '../schemas/i18n';
|
|
|
2
2
|
import builtinTranslations from '../translations/index';
|
|
3
3
|
import type { StarlightConfig } from './user-config';
|
|
4
4
|
|
|
5
|
-
export function createTranslationSystem(
|
|
6
|
-
userTranslations: Record<string,
|
|
5
|
+
export function createTranslationSystem<T extends i18nSchemaOutput>(
|
|
6
|
+
userTranslations: Record<string, T>,
|
|
7
7
|
config: Pick<StarlightConfig, 'defaultLocale' | 'locales'>
|
|
8
8
|
) {
|
|
9
9
|
/** User-configured default locale. */
|
|
@@ -67,18 +67,20 @@ function localeToLang(
|
|
|
67
67
|
return lang || defaultLang || 'en';
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
type BuiltInStrings = (typeof builtinTranslations)['en'];
|
|
71
|
+
|
|
70
72
|
/** Build a dictionary by layering preferred translation sources. */
|
|
71
|
-
function buildDictionary(
|
|
72
|
-
base:
|
|
73
|
-
...dictionaries: (
|
|
74
|
-
) {
|
|
73
|
+
function buildDictionary<T extends Record<string, string | undefined>>(
|
|
74
|
+
base: BuiltInStrings,
|
|
75
|
+
...dictionaries: (T | BuiltInStrings | undefined)[]
|
|
76
|
+
): BuiltInStrings & T {
|
|
75
77
|
const dictionary = { ...base };
|
|
76
78
|
// Iterate over alternate dictionaries to avoid overwriting preceding values with `undefined`.
|
|
77
79
|
for (const dict of dictionaries) {
|
|
78
80
|
for (const key in dict) {
|
|
79
|
-
const value = dict[key];
|
|
81
|
+
const value = dict[key as keyof typeof dict];
|
|
80
82
|
if (value) dictionary[key as keyof typeof dictionary] = value;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
|
-
return dictionary;
|
|
85
|
+
return dictionary as BuiltInStrings & T;
|
|
84
86
|
}
|
package/utils/route-data.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface PageProps extends Route {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface StarlightRouteData extends Route {
|
|
18
|
+
/** Title of the site. */
|
|
19
|
+
siteTitle: string;
|
|
18
20
|
/** Array of Markdown headings extracted from the current page. */
|
|
19
21
|
headings: MarkdownHeading[];
|
|
20
22
|
/** Site navigation sidebar entries for this page. */
|
|
@@ -40,10 +42,12 @@ export function generateRouteData({
|
|
|
40
42
|
props: PageProps;
|
|
41
43
|
url: URL;
|
|
42
44
|
}): StarlightRouteData {
|
|
43
|
-
const { entry, locale } = props;
|
|
45
|
+
const { entry, locale, lang } = props;
|
|
44
46
|
const sidebar = getSidebar(url.pathname, locale);
|
|
47
|
+
const siteTitle = getSiteTitle(lang);
|
|
45
48
|
return {
|
|
46
49
|
...props,
|
|
50
|
+
siteTitle,
|
|
47
51
|
sidebar,
|
|
48
52
|
hasSidebar: entry.data.template !== 'splash',
|
|
49
53
|
pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
|
|
@@ -105,3 +109,12 @@ function getEditUrl({ entry, id, isFallback }: PageProps): URL | undefined {
|
|
|
105
109
|
}
|
|
106
110
|
return url ? new URL(url) : undefined;
|
|
107
111
|
}
|
|
112
|
+
|
|
113
|
+
/** Get the site title for a given language. **/
|
|
114
|
+
export function getSiteTitle(lang: string): string {
|
|
115
|
+
const defaultLang = config.defaultLocale.lang as string;
|
|
116
|
+
if (lang && config.title[lang]) {
|
|
117
|
+
return config.title[lang] as string;
|
|
118
|
+
}
|
|
119
|
+
return config.title[defaultLang] as string;
|
|
120
|
+
}
|
package/utils/routing.ts
CHANGED
|
@@ -45,12 +45,15 @@ interface Path extends GetStaticPathsItem {
|
|
|
45
45
|
const normalizeIndexSlug = (slug: string) => (slug === 'index' ? '' : slug);
|
|
46
46
|
|
|
47
47
|
/** All entries in the docs content collection. */
|
|
48
|
-
const docs: StarlightDocsEntry[] = (
|
|
49
|
-
(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
})
|
|
53
|
-
)
|
|
48
|
+
const docs: StarlightDocsEntry[] = (
|
|
49
|
+
(await getCollection('docs', ({ data }) => {
|
|
50
|
+
// In production, filter out drafts.
|
|
51
|
+
return import.meta.env.MODE !== 'production' || data.draft === false;
|
|
52
|
+
})) ?? []
|
|
53
|
+
).map(({ slug, ...entry }) => ({
|
|
54
|
+
...entry,
|
|
55
|
+
slug: normalizeIndexSlug(slug),
|
|
56
|
+
}));
|
|
54
57
|
|
|
55
58
|
function getRoutes(): Route[] {
|
|
56
59
|
const routes: Route[] = docs.map((entry) => ({
|
package/utils/starlight-page.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type ContentConfig, type SchemaContext } from 'astro:content';
|
|
|
3
3
|
import config from 'virtual:starlight/user-config';
|
|
4
4
|
import { parseWithFriendlyErrors } from './error-map';
|
|
5
5
|
import { stripLeadingAndTrailingSlashes } from './path';
|
|
6
|
-
import { getToC, type PageProps, type StarlightRouteData } from './route-data';
|
|
6
|
+
import { getSiteTitle, getToC, type PageProps, type StarlightRouteData } from './route-data';
|
|
7
7
|
import type { StarlightDocsEntry } from './routing';
|
|
8
8
|
import { slugToLocaleData, urlToSlug } from './slugs';
|
|
9
9
|
import { getPrevNextLinks, getSidebar } from './navigation';
|
|
@@ -223,6 +223,7 @@ export async function generateStarlightPageRouteData({
|
|
|
223
223
|
lastUpdated,
|
|
224
224
|
pagination: getPrevNextLinks(sidebar, config.pagination, entry.data),
|
|
225
225
|
sidebar,
|
|
226
|
+
siteTitle: getSiteTitle(localeData.lang),
|
|
226
227
|
slug,
|
|
227
228
|
toc: getToC({
|
|
228
229
|
...routeProps,
|
package/utils/translations.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { getCollection } from 'astro:content';
|
|
1
|
+
import { getCollection, type CollectionEntry, type DataCollectionKey } from 'astro:content';
|
|
2
2
|
import config from 'virtual:starlight/user-config';
|
|
3
3
|
import type { i18nSchemaOutput } from '../schemas/i18n';
|
|
4
4
|
import { createTranslationSystem } from './createTranslationSystem';
|
|
5
5
|
|
|
6
|
+
type UserI18nSchema = 'i18n' extends DataCollectionKey
|
|
7
|
+
? CollectionEntry<'i18n'>['data']
|
|
8
|
+
: i18nSchemaOutput;
|
|
9
|
+
|
|
6
10
|
/** Get all translation data from the i18n collection, keyed by `id`, which matches locale. */
|
|
7
11
|
async function loadTranslations() {
|
|
8
|
-
let userTranslations: Record<string,
|
|
12
|
+
let userTranslations: Record<string, UserI18nSchema> = {};
|
|
9
13
|
// Briefly override `console.warn()` to silence logging when a project has no i18n collection.
|
|
10
14
|
const warn = console.warn;
|
|
11
15
|
console.warn = () => {};
|
package/utils/user-config.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { LogoConfigSchema } from '../schemas/logo';
|
|
|
8
8
|
import { SidebarItemSchema } from '../schemas/sidebar';
|
|
9
9
|
import { SocialLinksSchema } from '../schemas/social';
|
|
10
10
|
import { TableOfContentsSchema } from '../schemas/tableOfContents';
|
|
11
|
+
import { TitleConfigSchema, TitleTransformConfigSchema } from '../schemas/site-title';
|
|
11
12
|
|
|
12
13
|
const LocaleSchema = z.object({
|
|
13
14
|
/** The label for this language to show in UI, e.g. `"English"`, `"العربية"`, or `"简体中文"`. */
|
|
@@ -33,9 +34,7 @@ const LocaleSchema = z.object({
|
|
|
33
34
|
|
|
34
35
|
const UserConfigSchema = z.object({
|
|
35
36
|
/** Title for your website. Will be used in metadata and as browser tab title. */
|
|
36
|
-
title:
|
|
37
|
-
.string()
|
|
38
|
-
.describe('Title for your website. Will be used in metadata and as browser tab title.'),
|
|
37
|
+
title: TitleConfigSchema(),
|
|
39
38
|
|
|
40
39
|
/** Description metadata for your website. Can be used in page metadata. */
|
|
41
40
|
description: z
|
|
@@ -211,7 +210,7 @@ const UserConfigSchema = z.object({
|
|
|
211
210
|
});
|
|
212
211
|
|
|
213
212
|
export const StarlightConfigSchema = UserConfigSchema.strict().transform(
|
|
214
|
-
({ locales, defaultLocale, ...config }, ctx) => {
|
|
213
|
+
({ title, locales, defaultLocale, ...config }, ctx) => {
|
|
215
214
|
const configuredLocales = Object.keys(locales ?? {});
|
|
216
215
|
|
|
217
216
|
// This is a multilingual site (more than one locale configured) or a monolingual site with
|
|
@@ -236,8 +235,13 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
|
|
|
236
235
|
return z.NEVER;
|
|
237
236
|
}
|
|
238
237
|
|
|
238
|
+
// Transform the title
|
|
239
|
+
const TitleSchema = TitleTransformConfigSchema(defaultLocaleConfig.lang as string);
|
|
240
|
+
const parsedTitle = TitleSchema.parse(title);
|
|
241
|
+
|
|
239
242
|
return {
|
|
240
243
|
...config,
|
|
244
|
+
title: parsedTitle,
|
|
241
245
|
/** Flag indicating if this site has multiple locales set up. */
|
|
242
246
|
isMultilingual: configuredLocales.length > 1,
|
|
243
247
|
/** Full locale object for this site’s default language. */
|
|
@@ -248,18 +252,23 @@ export const StarlightConfigSchema = UserConfigSchema.strict().transform(
|
|
|
248
252
|
|
|
249
253
|
// This is a monolingual site with no locales configured or only a root locale, so things are
|
|
250
254
|
// pretty simple.
|
|
255
|
+
/** Full locale object for this site’s default language. */
|
|
256
|
+
const defaultLocaleConfig = {
|
|
257
|
+
label: 'English',
|
|
258
|
+
lang: 'en',
|
|
259
|
+
dir: 'ltr',
|
|
260
|
+
locale: undefined,
|
|
261
|
+
...locales?.root,
|
|
262
|
+
};
|
|
263
|
+
/** Transform the title */
|
|
264
|
+
const TitleSchema = TitleTransformConfigSchema(defaultLocaleConfig.lang);
|
|
265
|
+
const parsedTitle = TitleSchema.parse(title);
|
|
251
266
|
return {
|
|
252
267
|
...config,
|
|
268
|
+
title: parsedTitle,
|
|
253
269
|
/** Flag indicating if this site has multiple locales set up. */
|
|
254
270
|
isMultilingual: false,
|
|
255
|
-
|
|
256
|
-
defaultLocale: {
|
|
257
|
-
label: 'English',
|
|
258
|
-
lang: 'en',
|
|
259
|
-
dir: 'ltr',
|
|
260
|
-
locale: undefined,
|
|
261
|
-
...locales?.root,
|
|
262
|
-
},
|
|
271
|
+
defaultLocale: defaultLocaleConfig,
|
|
263
272
|
locales: undefined,
|
|
264
273
|
} as const;
|
|
265
274
|
}
|
package/virtual.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ declare module 'virtual:starlight/components/FallbackContentNotice' {
|
|
|
44
44
|
const FallbackContentNotice: typeof import('./components/FallbackContentNotice.astro').default;
|
|
45
45
|
export default FallbackContentNotice;
|
|
46
46
|
}
|
|
47
|
+
declare module 'virtual:starlight/components/DraftContentNotice' {
|
|
48
|
+
const DraftContentNotice: typeof import('./components/DraftContentNotice.astro').default;
|
|
49
|
+
export default DraftContentNotice;
|
|
50
|
+
}
|
|
47
51
|
|
|
48
52
|
declare module 'virtual:starlight/components/Footer' {
|
|
49
53
|
const Footer: typeof import('./components/Footer.astro').default;
|