@astrojs/starlight 0.0.3 → 0.0.4
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 +8 -0
- package/components/HeadSEO.astro +2 -0
- package/components.ts +2 -0
- package/index.ts +2 -1
- package/integrations/sitemap.ts +22 -0
- package/package.json +6 -2
- package/user-components/TabItem.astro +17 -0
- package/user-components/Tabs.astro +148 -0
- package/user-components/rehype-tabs.ts +82 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @astrojs/starlight
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#40](https://github.com/withastro/starlight/pull/40) [`e22dd76`](https://github.com/withastro/starlight/commit/e22dd76136f9749bb5d43f96241385faccfc90a1) Thanks [@delucis](https://github.com/delucis)! - Generate sitemaps for Starlight sites
|
|
8
|
+
|
|
9
|
+
- [#38](https://github.com/withastro/starlight/pull/38) [`623b577`](https://github.com/withastro/starlight/commit/623b577319b1dea2d6c42f1b680139fb858d85d6) Thanks [@delucis](https://github.com/delucis)! - Add tab components for use in MDX.
|
|
10
|
+
|
|
3
11
|
## 0.0.3
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/components/HeadSEO.astro
CHANGED
|
@@ -40,6 +40,8 @@ const description = data.description || config.description;
|
|
|
40
40
|
href={import.meta.env.BASE_URL + 'favicon.svg'}
|
|
41
41
|
type="image/svg+xml"
|
|
42
42
|
/>
|
|
43
|
+
{/* Link to sitemap, but only when `site` is set. */}
|
|
44
|
+
{Astro.site && <link rel="sitemap" href="/sitemap-index.xml" />}
|
|
43
45
|
|
|
44
46
|
<!-- OpenGraph Tags -->
|
|
45
47
|
<meta property="og:title" content={title} />
|
package/components.ts
ADDED
package/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { spawn } from 'node:child_process';
|
|
|
9
9
|
import { dirname, relative } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { starlightAsides } from './integrations/asides';
|
|
12
|
+
import { starlightSitemap } from './integrations/sitemap';
|
|
12
13
|
import {
|
|
13
14
|
StarlightUserConfig,
|
|
14
15
|
StarlightConfig,
|
|
@@ -63,7 +64,7 @@ export default function StarlightIntegration(
|
|
|
63
64
|
},
|
|
64
65
|
};
|
|
65
66
|
|
|
66
|
-
return [Starlight, mdx()];
|
|
67
|
+
return [starlightSitemap(userConfig), Starlight, mdx()];
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function resolveVirtualModuleId(id: string) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import sitemap, { SitemapOptions } from '@astrojs/sitemap';
|
|
2
|
+
import type { StarlightConfig } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A wrapped version of the `@astrojs/sitemap` integration configured based
|
|
6
|
+
* on Starlight i18n config.
|
|
7
|
+
*/
|
|
8
|
+
export function starlightSitemap(opts: StarlightConfig) {
|
|
9
|
+
const sitemapConfig: SitemapOptions = {};
|
|
10
|
+
if (opts.isMultilingual) {
|
|
11
|
+
sitemapConfig.i18n = {
|
|
12
|
+
defaultLocale: opts.defaultLocale.locale! || 'root',
|
|
13
|
+
locales: Object.fromEntries(
|
|
14
|
+
Object.entries(opts.locales).map(([locale, config]) => [
|
|
15
|
+
locale,
|
|
16
|
+
config?.lang!,
|
|
17
|
+
])
|
|
18
|
+
),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return sitemap(sitemapConfig);
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrojs/starlight",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Build beautiful, high-performance documentation websites with Astro",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"type": "module",
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./index.ts",
|
|
23
|
+
"./components": "./components.ts",
|
|
23
24
|
"./schema": "./schema.ts",
|
|
24
25
|
"./types": "./types.ts",
|
|
25
26
|
"./index.astro": "./index.astro",
|
|
@@ -34,16 +35,19 @@
|
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"@astrojs/mdx": "^0.19.1",
|
|
38
|
+
"@astrojs/sitemap": "^1.3.1",
|
|
37
39
|
"@pagefind/default-ui": "^0.12.0",
|
|
38
40
|
"@types/mdast": "^3.0.11",
|
|
39
41
|
"bcp-47": "^2.1.0",
|
|
40
42
|
"execa": "^7.1.1",
|
|
41
43
|
"hastscript": "^7.2.0",
|
|
42
44
|
"pagefind": "^0.12.0",
|
|
45
|
+
"rehype": "^12.0.1",
|
|
43
46
|
"remark-directive": "^2.0.1",
|
|
44
47
|
"unified": "^10.1.2",
|
|
45
48
|
"unist-util-remove": "^3.1.1",
|
|
46
|
-
"unist-util-visit": "^4.1.2"
|
|
49
|
+
"unist-util-visit": "^4.1.2",
|
|
50
|
+
"vfile": "^5.3.7"
|
|
47
51
|
},
|
|
48
52
|
"scripts": {}
|
|
49
53
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { TabItemTagname } from './rehype-tabs';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { label } = Astro.props;
|
|
9
|
+
|
|
10
|
+
if (!label) {
|
|
11
|
+
throw new Error('Missing prop `label` on `<TabItem>` component.');
|
|
12
|
+
}
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<TabItemTagname data-label={label}>
|
|
16
|
+
<slot />
|
|
17
|
+
</TabItemTagname>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { processPanels } from './rehype-tabs';
|
|
3
|
+
|
|
4
|
+
const panelHtml = await Astro.slots.render('default');
|
|
5
|
+
const { html, panels } = processPanels(panelHtml);
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<starlight-tabs>
|
|
9
|
+
{
|
|
10
|
+
panels && (
|
|
11
|
+
<div class="tablist-wrapper">
|
|
12
|
+
<ul role="tablist">
|
|
13
|
+
{panels.map(({ label, panelId, tabId }, idx) => (
|
|
14
|
+
<li role="presentation" class="tab">
|
|
15
|
+
<a
|
|
16
|
+
role="tab"
|
|
17
|
+
href={'#' + panelId}
|
|
18
|
+
id={tabId}
|
|
19
|
+
aria-selected={idx === 0 && 'true'}
|
|
20
|
+
tabindex={idx !== 0 ? -1 : 0}
|
|
21
|
+
>
|
|
22
|
+
{label}
|
|
23
|
+
</a>
|
|
24
|
+
</li>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
<Fragment set:html={html} />
|
|
31
|
+
</starlight-tabs>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
starlight-tabs {
|
|
35
|
+
display: block;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.tablist-wrapper {
|
|
39
|
+
overflow-x: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[role='tablist'] {
|
|
43
|
+
display: flex;
|
|
44
|
+
list-style: none;
|
|
45
|
+
border-bottom: 2px solid var(--sl-color-gray-5);
|
|
46
|
+
padding: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
[role='tablist'] .tab + .tab {
|
|
50
|
+
margin-top: 0;
|
|
51
|
+
}
|
|
52
|
+
.tab {
|
|
53
|
+
margin-bottom: -2px;
|
|
54
|
+
}
|
|
55
|
+
.tab > [role='tab'] {
|
|
56
|
+
display: block;
|
|
57
|
+
padding: 0 1.25rem;
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
border-bottom: 2px solid var(--sl-color-gray-5);
|
|
60
|
+
color: var(--sl-color-gray-3);
|
|
61
|
+
}
|
|
62
|
+
.tab [role='tab'][aria-selected] {
|
|
63
|
+
color: var(--sl-color-white);
|
|
64
|
+
border-color: var(--sl-color-text-accent);
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.tablist-wrapper ~ :global([role='tabpanel']) {
|
|
69
|
+
margin-top: 1rem;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
|
72
|
+
|
|
73
|
+
<script>
|
|
74
|
+
class StarlightTabs extends HTMLElement {
|
|
75
|
+
tabs: HTMLAnchorElement[];
|
|
76
|
+
panels: HTMLElement[];
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
super();
|
|
80
|
+
const tablist = this.querySelector<HTMLUListElement>('[role="tablist"]')!;
|
|
81
|
+
this.tabs = [
|
|
82
|
+
...tablist.querySelectorAll<HTMLAnchorElement>('[role="tab"]'),
|
|
83
|
+
];
|
|
84
|
+
this.panels = [
|
|
85
|
+
...this.querySelectorAll<HTMLElement>('[role="tabpanel"]'),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
this.tabs.forEach((tab, i) => {
|
|
89
|
+
// Handle clicks for mouse users
|
|
90
|
+
tab.addEventListener('click', (e) => {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
const currentTab = tablist.querySelector('[aria-selected]');
|
|
93
|
+
if (e.currentTarget !== currentTab) {
|
|
94
|
+
this.switchTab(e.currentTarget as HTMLAnchorElement, i);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Handle keyboard input
|
|
99
|
+
tab.addEventListener('keydown', (e) => {
|
|
100
|
+
const index = this.tabs.indexOf(e.currentTarget as any);
|
|
101
|
+
// Work out which key the user is pressing and
|
|
102
|
+
// Calculate the new tab's index where appropriate
|
|
103
|
+
const dir =
|
|
104
|
+
e.key === 'ArrowLeft'
|
|
105
|
+
? index - 1
|
|
106
|
+
: e.key === 'ArrowRight'
|
|
107
|
+
? index + 1
|
|
108
|
+
: e.key === 'ArrowDown'
|
|
109
|
+
? 'down'
|
|
110
|
+
: null;
|
|
111
|
+
if (dir === null) return;
|
|
112
|
+
// If the down key is pressed, move focus to the open panel,
|
|
113
|
+
// otherwise switch to the adjacent tab
|
|
114
|
+
if (dir === 'down') {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
this.panels[i]?.focus();
|
|
117
|
+
} else if (this.tabs[dir]) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
this.switchTab(this.tabs[dir], dir);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
switchTab(newTab: HTMLAnchorElement | null | undefined, index: number) {
|
|
126
|
+
if (!newTab) return;
|
|
127
|
+
|
|
128
|
+
// Mark all tabs as unselected and hide all tab panels.
|
|
129
|
+
this.tabs.forEach((tab) => {
|
|
130
|
+
tab.removeAttribute('aria-selected');
|
|
131
|
+
tab.setAttribute('tabindex', '-1');
|
|
132
|
+
});
|
|
133
|
+
this.panels.forEach((oldPanel) => {
|
|
134
|
+
oldPanel.hidden = true;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Show new panel and mark new tab as selected.
|
|
138
|
+
const newPanel = this.panels[index];
|
|
139
|
+
if (newPanel) newPanel.hidden = false;
|
|
140
|
+
// Restore active tab to the default tab order.
|
|
141
|
+
newTab.removeAttribute('tabindex');
|
|
142
|
+
newTab.setAttribute('aria-selected', 'true');
|
|
143
|
+
newTab.focus();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
customElements.define('starlight-tabs', StarlightTabs);
|
|
148
|
+
</script>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { rehype } from 'rehype';
|
|
2
|
+
import { CONTINUE, SKIP, visit } from 'unist-util-visit';
|
|
3
|
+
|
|
4
|
+
interface Panel {
|
|
5
|
+
panelId: string;
|
|
6
|
+
tabId: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module 'vfile' {
|
|
11
|
+
interface DataMap {
|
|
12
|
+
panels: Panel[];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TabItemTagname = 'starlight-tab-item';
|
|
17
|
+
|
|
18
|
+
let count = 0;
|
|
19
|
+
const getIDs = () => {
|
|
20
|
+
const id = count++;
|
|
21
|
+
return { panelId: 'tab-panel-' + id, tabId: 'tab-' + id };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rehype processor to extract tab panel data and turn each
|
|
26
|
+
* `<starlight-tab-item>` into a `<section>` with the necessary
|
|
27
|
+
* attributes.
|
|
28
|
+
*/
|
|
29
|
+
const tabsProcessor = rehype()
|
|
30
|
+
.data('settings', { fragment: true })
|
|
31
|
+
.use(function tabs() {
|
|
32
|
+
return (tree, file) => {
|
|
33
|
+
file.data.panels = [];
|
|
34
|
+
let isFirst = true;
|
|
35
|
+
visit(tree, 'element', (node) => {
|
|
36
|
+
if (node.tagName !== TabItemTagname || !node.properties) {
|
|
37
|
+
return CONTINUE;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { dataLabel } = node.properties;
|
|
41
|
+
const ids = getIDs();
|
|
42
|
+
file.data.panels?.push({
|
|
43
|
+
...ids,
|
|
44
|
+
label: String(dataLabel),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Remove `<TabItem>` props
|
|
48
|
+
delete node.properties.dataLabel;
|
|
49
|
+
// Turn into `<section>` with required attributes
|
|
50
|
+
node.tagName = 'section';
|
|
51
|
+
node.properties.id = ids.panelId;
|
|
52
|
+
node.properties['aria-labelledby'] = ids.tabId;
|
|
53
|
+
node.properties.role = 'tabpanel';
|
|
54
|
+
node.properties.tabindex = -1;
|
|
55
|
+
// Hide all panels except the first
|
|
56
|
+
// TODO: make initially visible tab configurable
|
|
57
|
+
if (isFirst) {
|
|
58
|
+
isFirst = false;
|
|
59
|
+
} else {
|
|
60
|
+
node.properties.hidden = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Skip over the tab panel’s children.
|
|
64
|
+
return SKIP;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Process tab panel items to extract data for the tab links and format
|
|
71
|
+
* each tab panel correctly.
|
|
72
|
+
* @param html Inner HTML passed to the `<Tabs>` component.
|
|
73
|
+
*/
|
|
74
|
+
export const processPanels = (html: string) => {
|
|
75
|
+
const file = tabsProcessor.processSync({ value: html });
|
|
76
|
+
return {
|
|
77
|
+
/** Data for each tab panel. */
|
|
78
|
+
panels: file.data.panels,
|
|
79
|
+
/** Processed HTML for the tab panels. */
|
|
80
|
+
html: file.toString(),
|
|
81
|
+
};
|
|
82
|
+
};
|