@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 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
@@ -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
@@ -0,0 +1,2 @@
1
+ export { default as Tabs } from './user-components/Tabs.astro';
2
+ export { default as TabItem } from './user-components/TabItem.astro';
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",
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
+ };