@astrojs/starlight 0.4.2 → 0.5.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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # @astrojs/starlight
2
2
 
3
+ ## 0.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#336](https://github.com/withastro/starlight/pull/336) [`2b3302b`](https://github.com/withastro/starlight/commit/2b3302b80451f318fb05a5e8a7284feb28999e66) Thanks [@delucis](https://github.com/delucis)! - Add support for LinkedIn, Threads, and Twitch social icon links
8
+
9
+ - [#335](https://github.com/withastro/starlight/pull/335) [`757c65f`](https://github.com/withastro/starlight/commit/757c65ffc468fd2c782312b476fa7659d0cfd198) Thanks [@delucis](https://github.com/delucis)! - Fix relative path resolution on Windows
10
+
11
+ - [#332](https://github.com/withastro/starlight/pull/332) [`0600c1a`](https://github.com/withastro/starlight/commit/0600c1a917bf86efa6b2d053aa47e3a4b17e8049) Thanks [@sasoria](https://github.com/sasoria)! - Add Norwegian UI translations
12
+
13
+ - [#328](https://github.com/withastro/starlight/pull/328) [`e478848`](https://github.com/withastro/starlight/commit/e478848de1c41a46f58d0ac0d62d7b7272cf1241) Thanks [@astridx](https://github.com/astridx)! - Add missing accessible labels for Codeberg and YouTube social links
14
+
15
+ ## 0.5.0
16
+
17
+ ### Minor Changes
18
+
19
+ - [#313](https://github.com/withastro/starlight/pull/313) [`dc42569`](https://github.com/withastro/starlight/commit/dc42569bddfae2c48ea60c0dd5cc70643a129a68) Thanks [@delucis](https://github.com/delucis)! - Add a `not-content` CSS class that allows users to opt out of Starlight’s default content styling
20
+
21
+ - [#297](https://github.com/withastro/starlight/pull/297) [`fb15a9b`](https://github.com/withastro/starlight/commit/fb15a9b65252ac5fa32304096fbdb49ecdd6009b) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Improve `<Tabs>` component keyboard interactions
22
+
23
+ - [#303](https://github.com/withastro/starlight/pull/303) [`69b7d4c`](https://github.com/withastro/starlight/commit/69b7d4c23761a45dc2b9ea75c6c9c904a885ba5d) Thanks [@HiDeoo](https://github.com/HiDeoo)! - Add new global `pagination` option defaulting to `true` to define whether or not the previous and next page links are shown in the footer. A page can override this setting or the link text and/or URL using the new `prev` and `next` frontmatter fields.
24
+
25
+ ### Patch Changes
26
+
27
+ - [#318](https://github.com/withastro/starlight/pull/318) [`5db3e6e`](https://github.com/withastro/starlight/commit/5db3e6ea2e5cb7d9552fc54567811358851fb533) Thanks [@delucis](https://github.com/delucis)! - Support relative paths in Starlight config for `customCSS` and `logo` paths
28
+
3
29
  ## 0.4.2
4
30
 
5
31
  ### Patch Changes
@@ -14,7 +14,10 @@ interface Props extends LocaleData {
14
14
  }
15
15
 
16
16
  const { entry, dir, lang, locale, sidebar } = Astro.props;
17
- const prevNextLinks = getPrevNextLinks(sidebar);
17
+ const prevNextLinks = getPrevNextLinks(sidebar, config.pagination, {
18
+ prev: entry.data.prev,
19
+ next: entry.data.next
20
+ });
18
21
  ---
19
22
 
20
23
  <footer>
@@ -64,4 +64,10 @@ export const Icons = {
64
64
  '<path d="M12 .5a12 12 0 0 0-12 12 12 12 0 0 0 1.8 6.4l10-13a.2.1 0 0 1 .4 0l10 13a12 12 0 0 0 1.8-6.4 12 12 0 0 0-12-12zm.3 6.5 4.4 16.5a12 12 0 0 0 5.2-4.2z"/>',
65
65
  youtube:
66
66
  '<path d="M23.5 6.2A3 3 0 0 0 21.4 4c-1.9-.5-9.4-.5-9.4-.5s-7.5 0-9.4.5A3 3 0 0 0 .5 6.3C0 8 0 12 0 12s0 4 .5 5.8A3 3 0 0 0 2.6 20c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 0 0 2.1-2c.5-2 .5-5.9.5-5.9s0-4-.5-5.8zm-14 9.4V8.4l6.3 3.6-6.3 3.6z"/>',
67
+ threads:
68
+ '<path d="m17.73 11.2-.29-.13c-.17-3.13-1.88-4.92-4.75-4.94h-.04c-1.72 0-3.14.73-4.02 2.06l1.58 1.09a2.8 2.8 0 0 1 2.47-1.21c.94 0 1.66.28 2.12.81.33.4.56.93.67 1.61-.84-.14-1.74-.18-2.71-.13-2.73.16-4.49 1.75-4.37 3.97a3.41 3.41 0 0 0 1.57 2.71c.81.54 1.85.8 2.93.74a4.32 4.32 0 0 0 3.33-1.62 6 6 0 0 0 1.14-2.97 3.5 3.5 0 0 1 1.46 1.6 4 4 0 0 1-.98 4.4c-1.3 1.3-2.86 1.85-5.21 1.87-2.62-.02-4.6-.86-5.88-2.5-1.2-1.52-1.82-3.73-1.85-6.56.03-2.83.65-5.04 1.85-6.57 1.29-1.63 3.26-2.47 5.88-2.49 2.63.02 4.64.86 5.97 2.5.66.8 1.15 1.82 1.48 3l1.85-.5c-.4-1.44-1.02-2.7-1.86-3.73-1.71-2.1-4.21-3.19-7.44-3.21h-.01c-3.22.02-5.7 1.1-7.35 3.22C3.79 6.1 3.03 8.72 3 11.99V12c.03 3.29.79 5.9 2.27 7.78 1.66 2.12 4.13 3.2 7.35 3.22h.01c2.86-.02 4.88-.77 6.54-2.43a5.95 5.95 0 0 0 1.4-6.56 5.62 5.62 0 0 0-2.84-2.81Zm-4.94 4.64c-1.2.07-2.44-.47-2.5-1.62-.05-.85.6-1.8 2.57-1.92l.67-.02c.71 0 1.38.07 1.99.2-.23 2.84-1.56 3.3-2.73 3.36Z"/>',
69
+ linkedin:
70
+ '<path d="M20.47 2H3.53a1.45 1.45 0 0 0-1.47 1.43v17.14A1.45 1.45 0 0 0 3.53 22h16.94a1.45 1.45 0 0 0 1.47-1.43V3.43A1.45 1.45 0 0 0 20.47 2ZM8.09 18.74h-3v-9h3v9ZM6.59 8.48a1.56 1.56 0 0 1 0-3.12 1.57 1.57 0 1 1 0 3.12Zm12.32 10.26h-3v-4.83c0-1.21-.43-2-1.52-2A1.65 1.65 0 0 0 12.85 13a2 2 0 0 0-.1.73v5h-3v-9h3V11a3 3 0 0 1 2.71-1.5c2 0 3.45 1.29 3.45 4.06v5.18Z"/>',
71
+ twitch:
72
+ '<path d="M2.5 1 1 4.8v15.4h5.5V23h3.1l3-2.8H17l6-5.7V1H2.6ZM21 13.5l-3.4 3.3H12l-3 2.8v-2.8H4.5V3H21v10.5Zm-3.4-6.8v5.8h-2V6.7h2Zm-5.5 0v5.8h-2V6.7h2Z"/>',
67
73
  };
@@ -2,78 +2,94 @@
2
2
 
3
3
  <style>
4
4
  .content
5
- :global(
5
+ > :global(
6
6
  :not(a, strong, em, del, span, input)
7
- + :not(a, strong, em, del, span, input)
7
+ + :not(a, strong, em, del, span, input, :where(.not-content *))
8
8
  ) {
9
9
  margin-top: 1.5rem;
10
10
  }
11
11
 
12
12
  /* Headings after non-headings have more spacing. */
13
- .content :global(:not(h1, h2, h3, h4, h5, h6) + :is(h1, h2, h3, h4, h5, h6)) {
13
+ .content
14
+ :global(
15
+ :not(h1, h2, h3, h4, h5, h6)
16
+ + :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *))
17
+ ) {
14
18
  margin-top: 2.5rem;
15
19
  }
16
20
 
17
- .content :global(li + li),
18
- .content :global(dt + dt),
19
- .content :global(dt + dd),
20
- .content :global(dd + dd) {
21
+ .content :global(li + li:not(:where(.not-content *))),
22
+ .content :global(dt + dt:not(:where(.not-content *))),
23
+ .content :global(dt + dd:not(:where(.not-content *))),
24
+ .content :global(dd + dd:not(:where(.not-content *))) {
21
25
  margin-top: 0.25rem;
22
26
  }
23
27
 
24
28
  .content
25
29
  :global(
26
- li > :last-child:not(li, ul, ol):not(a, strong, em, del, span, input)
30
+ li
31
+ > :last-child:not(li, ul, ol):not(
32
+ a,
33
+ strong,
34
+ em,
35
+ del,
36
+ span,
37
+ input,
38
+ :where(.not-content *)
39
+ )
27
40
  ) {
28
41
  margin-bottom: 1.25rem;
29
42
  }
30
43
 
31
- .content :global(dt) {
44
+ .content :global(dt:not(:where(.not-content *))) {
32
45
  font-weight: 700;
33
46
  }
34
- .content :global(dd) {
47
+ .content :global(dd:not(:where(.not-content *))) {
35
48
  padding-inline-start: 1rem;
36
49
  }
37
50
 
38
- .content :global(:is(h1, h2, h3, h4, h5, h6)) {
51
+ .content :global(:is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *))) {
39
52
  color: var(--sl-color-white);
40
53
  line-height: var(--sl-line-height-headings);
41
54
  font-weight: 600;
42
55
  }
43
56
 
44
- .content :global(:is(img, picture, video, canvas, svg, iframe)) {
57
+ .content
58
+ :global(
59
+ :is(img, picture, video, canvas, svg, iframe):not(:where(.not-content *))
60
+ ) {
45
61
  display: block;
46
62
  max-width: 100%;
47
63
  height: auto;
48
64
  }
49
65
 
50
- .content :global(h1) {
66
+ .content :global(h1:not(:where(.not-content *))) {
51
67
  font-size: var(--sl-text-h1);
52
68
  }
53
- .content :global(h2) {
69
+ .content :global(h2:not(:where(.not-content *))) {
54
70
  font-size: var(--sl-text-h2);
55
71
  }
56
- .content :global(h3) {
72
+ .content :global(h3:not(:where(.not-content *))) {
57
73
  font-size: var(--sl-text-h3);
58
74
  }
59
- .content :global(h4) {
75
+ .content :global(h4:not(:where(.not-content *))) {
60
76
  font-size: var(--sl-text-h4);
61
77
  }
62
- .content :global(h5) {
78
+ .content :global(h5:not(:where(.not-content *))) {
63
79
  font-size: var(--sl-text-h5);
64
80
  }
65
- .content :global(h6) {
81
+ .content :global(h6:not(:where(.not-content *))) {
66
82
  font-size: var(--sl-text-h6);
67
83
  }
68
84
 
69
- .content :global(a) {
85
+ .content :global(a:not(:where(.not-content *))) {
70
86
  color: var(--sl-color-text-accent);
71
87
  }
72
- .content :global(a:hover) {
88
+ .content :global(a:hover:not(:where(.not-content *))) {
73
89
  color: var(--sl-color-white);
74
90
  }
75
91
 
76
- .content :global(code) {
92
+ .content :global(code:not(:where(.not-content *))) {
77
93
  background-color: var(--sl-color-bg-inline-code);
78
94
  margin-block: -0.125rem;
79
95
  padding: 0.125rem 0.375rem;
@@ -83,36 +99,36 @@
83
99
  font-size: inherit;
84
100
  }
85
101
 
86
- .content :global(pre) {
102
+ .content :global(pre:not(:where(.not-content *))) {
87
103
  border: 1px solid var(--sl-color-gray-5);
88
104
  padding: 0.75rem 1rem;
89
105
  font-size: var(--sl-text-code);
90
106
  }
91
107
 
92
- .content :global(pre code) {
108
+ .content :global(pre code:not(:where(.not-content *))) {
93
109
  all: unset;
94
110
  font-family: var(--__sl-font-mono);
95
111
  }
96
112
 
97
- .content :global(blockquote) {
113
+ .content :global(blockquote:not(:where(.not-content *))) {
98
114
  border-inline-start: 1px solid var(--sl-color-gray-5);
99
115
  padding-inline-start: 1rem;
100
116
  }
101
117
 
102
- .content :global(table) {
118
+ .content :global(table:not(:where(.not-content *))) {
103
119
  display: block;
104
120
  overflow: auto;
105
121
  border-collapse: collapse;
106
122
  }
107
- .content :global(tr:nth-child(2n)) {
123
+ .content :global(tr:nth-child(2n):not(:where(.not-content *))) {
108
124
  background-color: var(--sl-color-gray-7, var(--sl-color-gray-6));
109
125
  }
110
- .content :global(:is(th, td)) {
126
+ .content :global(:is(th, td):not(:where(.not-content *))) {
111
127
  border: 1px solid var(--sl-color-hairline);
112
128
  padding: 0.375rem 0.8125rem;
113
129
  }
114
130
 
115
- .content :global(hr) {
131
+ .content :global(hr:not(:where(.not-content *))) {
116
132
  border: 0;
117
133
  border-bottom: 1px solid var(--sl-color-hairline);
118
134
  }
@@ -9,6 +9,11 @@ const labels: Record<Platform, string> = {
9
9
  discord: 'Discord',
10
10
  twitter: 'Twitter',
11
11
  mastodon: 'Mastodon',
12
+ codeberg: 'Codeberg',
13
+ youtube: 'YouTube',
14
+ threads: 'Threads',
15
+ linkedin: 'LinkedIn',
16
+ twitch: 'Twitch',
12
17
  };
13
18
 
14
19
  const links = Object.entries(config.social || {}).filter(([, url]) =>
package/index.ts CHANGED
@@ -6,7 +6,7 @@ import type {
6
6
  ViteUserConfig,
7
7
  } from 'astro';
8
8
  import { spawn } from 'node:child_process';
9
- import { dirname, relative } from 'node:path';
9
+ import { dirname, relative, resolve } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { starlightAsides } from './integrations/asides';
12
12
  import { starlightSitemap } from './integrations/sitemap';
@@ -88,18 +88,20 @@ function vitePluginStarlightUserConfig(
88
88
  opts: StarlightConfig,
89
89
  { root }: AstroConfig
90
90
  ): NonNullable<ViteUserConfig['plugins']>[number] {
91
+ const resolveRelativeId = (id: string) =>
92
+ id.startsWith('.') ? JSON.stringify(resolve(fileURLToPath(root), id)) : id;
91
93
  const modules = {
92
94
  'virtual:starlight/user-config': `export default ${JSON.stringify(opts)}`,
93
95
  'virtual:starlight/project-context': `export default ${JSON.stringify({
94
96
  root,
95
97
  })}`,
96
98
  'virtual:starlight/user-css': opts.customCss
97
- .map((id) => `import "${id}";`)
99
+ .map((id) => `import ${resolveRelativeId(id)};`)
98
100
  .join(''),
99
101
  'virtual:starlight/user-images': opts.logo
100
102
  ? 'src' in opts.logo
101
- ? `import src from "${opts.logo.src}"; export const logos = { dark: src, light: src };`
102
- : `import dark from "${opts.logo.dark}"; import light from "${opts.logo.light}"; export const logos = { dark, light };`
103
+ ? `import src from ${resolveRelativeId(opts.logo.src)}; export const logos = { dark: src, light: src };`
104
+ : `import dark from ${resolveRelativeId(opts.logo.dark)}; import light from ${resolveRelativeId(opts.logo.light)}; export const logos = { dark, light };`
103
105
  : 'export const logos = {};',
104
106
  };
105
107
  const resolutionMap = Object.fromEntries(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/starlight",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Build beautiful, high-performance documentation websites with Astro",
5
5
  "keywords": [
6
6
  "docs",
@@ -40,6 +40,7 @@
40
40
  "@types/mdast": "^3.0.11",
41
41
  "bcp-47": "^2.1.0",
42
42
  "execa": "^7.1.1",
43
+ "hast-util-select": "^5.0.5",
43
44
  "hastscript": "^7.2.0",
44
45
  "pagefind": "^1.0.0-alpha.5",
45
46
  "rehype": "^12.0.1",
package/schema.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'astro/zod';
2
2
  import { HeadConfigSchema } from './schemas/head';
3
+ import { PrevNextLinkConfigSchema } from './schemas/prevNextLink';
3
4
  import { TableOfContentsSchema } from './schemas/tableOfContents';
4
5
  import { Icons } from './components/Icons';
5
6
  export { i18nSchema } from './schemas/i18n';
@@ -119,5 +120,16 @@ export function docsSchema() {
119
120
  * Overrides the `lastUpdated` global config or the date generated from the Git history.
120
121
  */
121
122
  lastUpdated: z.union([z.date(), z.boolean()]).optional(),
123
+
124
+ /**
125
+ * The previous navigation link configuration.
126
+ * Overrides the `pagination` global config or the link text and/or URL.
127
+ */
128
+ prev: PrevNextLinkConfigSchema(),
129
+ /**
130
+ * The next navigation link configuration.
131
+ * Overrides the `pagination` global config or the link text and/or URL.
132
+ */
133
+ next: PrevNextLinkConfigSchema(),
122
134
  });
123
135
  }
@@ -0,0 +1,20 @@
1
+ import { z } from 'astro/zod';
2
+
3
+ export const PrevNextLinkConfigSchema = () =>
4
+ z
5
+ .union([
6
+ z.boolean(),
7
+ z.string(),
8
+ z
9
+ .object({
10
+ /** The navigation link URL. */
11
+ link: z.string().optional(),
12
+ /** The navigation link text. */
13
+ label: z.string().optional(),
14
+ })
15
+ .strict(),
16
+ ])
17
+ .optional();
18
+
19
+ export type PrevNextLinkUserConfig = z.input<ReturnType<typeof PrevNextLinkConfigSchema>>;
20
+ export type PrevNextLinkConfig = z.output<ReturnType<typeof PrevNextLinkConfigSchema>>;
@@ -11,11 +11,12 @@ import nl from './nl.json';
11
11
  import da from './da.json';
12
12
  import tr from './tr.json';
13
13
  import ar from './ar.json';
14
+ import nb from './nb.json';
14
15
 
15
16
  const { parse } = builtinI18nSchema();
16
17
 
17
18
  export default Object.fromEntries(
18
- Object.entries({ cs, en, es, de, ja, pt, fr, it, nl, da, tr, ar }).map(([key, dict]) => [
19
+ Object.entries({ cs, en, es, de, ja, pt, fr, it, nl, da, tr, ar, nb }).map(([key, dict]) => [
19
20
  key,
20
21
  parse(dict),
21
22
  ])
@@ -0,0 +1,22 @@
1
+ {
2
+ "skipLink.label": "Gå til innholdet",
3
+ "search.label": "Søk",
4
+ "search.shortcutLabel": "(Trykk / for å søke)",
5
+ "search.cancelLabel": "Avbryt",
6
+ "search.devWarning": "Søk er bare tilgjengelig i produksjonsbygg. \nPrøv å bygg siden og forhåndsvis den for å teste det lokalt.",
7
+ "themeSelect.accessibleLabel": "Velg tema",
8
+ "themeSelect.dark": "Mørk",
9
+ "themeSelect.light": "Lys",
10
+ "themeSelect.auto": "Auto",
11
+ "languageSelect.accessibleLabel": "Velg språk",
12
+ "menuButton.accessibleLabel": "Meny",
13
+ "sidebarNav.accessibleLabel": "Hovednavigasjon",
14
+ "tableOfContents.onThisPage": "På denne siden",
15
+ "tableOfContents.overview": "Oversikt",
16
+ "i18n.untranslatedContent": "Dette innholdet er ikke tilgjengelig på ditt språk.",
17
+ "page.editLink": "Rediger side",
18
+ "page.lastUpdated": "Sist oppdatert:",
19
+ "page.previousLink": "Forrige",
20
+ "page.nextLink": "Neste",
21
+ "404.text": "Siden ble ikke funnet. Sjekk URL-en eller prøv å bruke søkefeltet."
22
+ }
@@ -8,7 +8,7 @@ const { html, panels } = processPanels(panelHtml);
8
8
  <starlight-tabs>
9
9
  {
10
10
  panels && (
11
- <div class="tablist-wrapper">
11
+ <div class="tablist-wrapper not-content">
12
12
  <ul role="tablist">
13
13
  {panels.map(({ label, panelId, tabId }, idx) => (
14
14
  <li role="presentation" class="tab">
@@ -46,9 +46,6 @@ const { html, panels } = processPanels(panelHtml);
46
46
  padding: 0;
47
47
  }
48
48
 
49
- [role='tablist'] .tab + .tab {
50
- margin-top: 0;
51
- }
52
49
  .tab {
53
50
  margin-bottom: -2px;
54
51
  }
@@ -101,23 +98,20 @@ const { html, panels } = processPanels(panelHtml);
101
98
  const index = this.tabs.indexOf(e.currentTarget as any);
102
99
  // Work out which key the user is pressing and
103
100
  // Calculate the new tab's index where appropriate
104
- const dir =
101
+ const nextIndex =
105
102
  e.key === 'ArrowLeft'
106
103
  ? index - 1
107
104
  : e.key === 'ArrowRight'
108
105
  ? index + 1
109
- : e.key === 'ArrowDown'
110
- ? 'down'
106
+ : e.key === 'Home'
107
+ ? 0
108
+ : e.key === 'End'
109
+ ? this.tabs.length - 1
111
110
  : null;
112
- if (dir === null) return;
113
- // If the down key is pressed, move focus to the open panel,
114
- // otherwise switch to the adjacent tab
115
- if (dir === 'down') {
116
- e.preventDefault();
117
- this.panels[i]?.focus();
118
- } else if (this.tabs[dir]) {
111
+ if (nextIndex === null) return;
112
+ if (this.tabs[nextIndex]) {
119
113
  e.preventDefault();
120
- this.switchTab(this.tabs[dir], dir);
114
+ this.switchTab(this.tabs[nextIndex], nextIndex);
121
115
  }
122
116
  });
123
117
  });
@@ -1,3 +1,4 @@
1
+ import { select } from 'hast-util-select';
1
2
  import { rehype } from 'rehype';
2
3
  import { CONTINUE, SKIP, visit } from 'unist-util-visit';
3
4
 
@@ -15,6 +16,26 @@ declare module 'vfile' {
15
16
 
16
17
  export const TabItemTagname = 'starlight-tab-item';
17
18
 
19
+ // https://github.com/adobe/react-spectrum/blob/99ca82e87ba2d7fdd54f5b49326fd242320b4b51/packages/%40react-aria/focus/src/FocusScope.tsx#L256-L275
20
+ const focusableElementSelectors = [
21
+ 'input:not([disabled]):not([type=hidden])',
22
+ 'select:not([disabled])',
23
+ 'textarea:not([disabled])',
24
+ 'button:not([disabled])',
25
+ 'a[href]',
26
+ 'area[href]',
27
+ 'summary',
28
+ 'iframe',
29
+ 'object',
30
+ 'embed',
31
+ 'audio[controls]',
32
+ 'video[controls]',
33
+ '[contenteditable]',
34
+ '[tabindex]:not([disabled])',
35
+ ]
36
+ .map((selector) => `${selector}:not([hidden]):not([tabindex="-1"])`)
37
+ .join(',');
38
+
18
39
  let count = 0;
19
40
  const getIDs = () => {
20
41
  const id = count++;
@@ -51,7 +72,14 @@ const tabsProcessor = rehype()
51
72
  node.properties.id = ids.panelId;
52
73
  node.properties['aria-labelledby'] = ids.tabId;
53
74
  node.properties.role = 'tabpanel';
54
- node.properties.tabindex = -1;
75
+
76
+ const focusableChild = select(focusableElementSelectors, node);
77
+ // If the panel does not contain any focusable elements, include it in
78
+ // the tab sequence of the page.
79
+ if (!focusableChild) {
80
+ node.properties.tabindex = 0;
81
+ }
82
+
55
83
  // Hide all panels except the first
56
84
  // TODO: make initially visible tab configurable
57
85
  if (isFirst) {
@@ -9,6 +9,7 @@ import type {
9
9
  SidebarItem,
10
10
  SidebarLinkItem,
11
11
  } from './user-config';
12
+ import type { PrevNextLinkConfig } from '../schemas/prevNextLink';
12
13
 
13
14
  export interface Link {
14
15
  type: 'link';
@@ -234,17 +235,63 @@ export function flattenSidebar(sidebar: SidebarEntry[]): Link[] {
234
235
  );
235
236
  }
236
237
 
237
- /** Get previous/next pages in the sidebar if there are any. */
238
- export function getPrevNextLinks(sidebar: SidebarEntry[]): {
238
+ /** Get previous/next pages in the sidebar or the ones from the frontmatter if any. */
239
+ export function getPrevNextLinks(
240
+ sidebar: SidebarEntry[],
241
+ paginationEnabled: boolean,
242
+ config: {
243
+ prev?: PrevNextLinkConfig;
244
+ next?: PrevNextLinkConfig;
245
+ }
246
+ ): {
239
247
  prev: Link | undefined;
240
248
  next: Link | undefined;
241
249
  } {
242
250
  const entries = flattenSidebar(sidebar);
243
251
  const currentIndex = entries.findIndex((entry) => entry.isCurrent);
244
- const prev = entries[currentIndex - 1];
245
- const next = currentIndex > -1 ? entries[currentIndex + 1] : undefined;
252
+ const prev = applyPrevNextLinkConfig(
253
+ entries[currentIndex - 1],
254
+ paginationEnabled,
255
+ config.prev
256
+ );
257
+ const next = applyPrevNextLinkConfig(
258
+ currentIndex > -1 ? entries[currentIndex + 1] : undefined,
259
+ paginationEnabled,
260
+ config.next
261
+ );
246
262
  return { prev, next };
247
263
  }
248
264
 
265
+ /** Apply a prev/next link config to a navigation link. */
266
+ function applyPrevNextLinkConfig(
267
+ link: Link | undefined,
268
+ paginationEnabled: boolean,
269
+ config: PrevNextLinkConfig | undefined
270
+ ): Link | undefined {
271
+ // Explicitly remove the link.
272
+ if (config === false) return undefined;
273
+ // Use the generated link if any.
274
+ else if (config === true) return link;
275
+ // If a link exists, update its label if needed.
276
+ else if (typeof config === 'string' && link) {
277
+ return { ...link, label: config };
278
+ } else if (typeof config === 'object') {
279
+ if (link) {
280
+ // If a link exists, update both its label and href if needed.
281
+ return {
282
+ ...link,
283
+ label: config.label ?? link.label,
284
+ href: config.link ?? link.href,
285
+ }
286
+ } else if (config.link && config.label) {
287
+ // If there is no link and the frontmatter contains both a URL and a label,
288
+ // create a new link.
289
+ return makeLink(config.link, config.label, config.link);
290
+ }
291
+ }
292
+ // Otherwise, if the global config is enabled, return the generated link if any.
293
+ return paginationEnabled ? link : undefined;
294
+ }
295
+
249
296
  /** Remove the extension from a path. */
250
297
  const stripExtension = (path: string) => path.replace(/\.\w+$/, '');
@@ -53,14 +53,14 @@ const AutoSidebarGroupSchema = SidebarGroupSchema.extend({
53
53
  directory: z.string(),
54
54
  /**
55
55
  * Whether the autogenerated subgroups should be collapsed by default.
56
- * Defaults to the `AutoSidebarGroup` `collapsed` value.
56
+ * Defaults to the `AutoSidebarGroup` `collapsed` value.
57
57
  */
58
58
  collapsed: z.boolean().optional(),
59
59
  // TODO: not supported by Docusaurus but would be good to have
60
60
  /** How many directories deep to include from this directory in the sidebar. Default: `Infinity`. */
61
61
  // depth: z.number().optional(),
62
- })
63
- })
62
+ }),
63
+ });
64
64
  export type AutoSidebarGroup = z.infer<typeof AutoSidebarGroupSchema>;
65
65
 
66
66
  type ManualSidebarGroupInput = z.input<typeof SidebarGroupSchema> & {
@@ -96,7 +96,7 @@ const ManualSidebarGroupSchema: z.ZodType<
96
96
  ])
97
97
  .array()
98
98
  ),
99
- })
99
+ });
100
100
 
101
101
  const SidebarItemSchema = z.union([
102
102
  SidebarLinkItemSchema,
@@ -139,6 +139,12 @@ const UserConfigSchema = z.object({
139
139
  codeberg: z.string().url().optional(),
140
140
  /** Link to the Youtube channel for this site, e.g. `'https://www.youtube.com/@astrodotbuild'`. */
141
141
  youtube: z.string().url().optional(),
142
+ /** Link to the Threads profile for this site, e.g. `'https://www.threads.net/@nmoodev'`. */
143
+ threads: z.string().url().optional(),
144
+ /** Link to the LinkedIn page for this site, e.g. `'https://www.linkedin.com/company/astroinc'`. */
145
+ linkedin: z.string().url().optional(),
146
+ /** Link to the Twitch profile or repository for this site, e.g. `'https://www.twitch.tv/bholmesdev'`. */
147
+ twitch: z.string().url().optional(),
142
148
  })
143
149
  .optional(),
144
150
 
@@ -253,6 +259,14 @@ const UserConfigSchema = z.object({
253
259
  .describe(
254
260
  'Define if the last update date should be visible in the page footer.'
255
261
  ),
262
+
263
+ /** Define if the previous and next page links should be visible in the page footer. */
264
+ pagination: z
265
+ .boolean()
266
+ .default(true)
267
+ .describe(
268
+ 'Define if the previous and next page links should be visible in the page footer.'
269
+ ),
256
270
  });
257
271
 
258
272
  export const StarlightConfigSchema = UserConfigSchema.strict().transform(