reallysimpledocs 1.0.0-beta.4 → 1.0.0-beta.5

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 (49) hide show
  1. package/README.md +35 -35
  2. package/package.json +10 -13
  3. package/src/astro/index.js +46 -98
  4. package/src/astro/preview-source.js +90 -0
  5. package/src/components/index.js +1 -0
  6. package/src/css/custom.css +252 -33
  7. package/src/css/styles/luma.css +11 -0
  8. package/src/css/styles/lyra.css +11 -0
  9. package/src/css/styles/maia.css +11 -0
  10. package/src/css/styles/mira.css +11 -0
  11. package/src/css/styles/nova.css +11 -0
  12. package/src/css/styles/rhea.css +11 -0
  13. package/src/css/styles/sera.css +11 -0
  14. package/src/css/styles/vega.css +11 -0
  15. package/src/runtime/components/BaseLayout.astro +20 -27
  16. package/src/runtime/components/CommandDialog.astro +30 -32
  17. package/src/runtime/components/DefaultContentHeader.astro +0 -17
  18. package/src/runtime/components/DefaultHead.astro +120 -0
  19. package/src/runtime/components/DefaultHeadContent.astro +2 -0
  20. package/src/runtime/components/DefaultSidebarHeader.astro +1 -1
  21. package/src/runtime/components/DocsLayout.astro +114 -165
  22. package/src/runtime/components/DropdownMenu.astro +1 -1
  23. package/src/runtime/components/ThemeToggle.astro +21 -0
  24. package/src/runtime/components/mdx/Callout.astro +2 -32
  25. package/src/runtime/components/mdx/Card.astro +3 -8
  26. package/src/runtime/components/mdx/CardGrid.astro +1 -1
  27. package/src/runtime/components/mdx/Code.astro +2 -16
  28. package/src/runtime/components/mdx/CodeGroup.astro +7 -11
  29. package/src/runtime/components/mdx/Preview.astro +7 -13
  30. package/src/runtime/components/mdx/Steps.astro +1 -5
  31. package/src/runtime/components/mdx/Tab.astro +6 -0
  32. package/src/runtime/components/mdx/Tabs.astro +7 -7
  33. package/src/runtime/lib/docs.js +130 -102
  34. package/src/runtime/lib/highlight.js +1 -14
  35. package/src/runtime/lib/macros.js +10 -5
  36. package/src/runtime/lib/markdown-export.js +134 -171
  37. package/src/runtime/pages/doc.astro +1 -3
  38. package/src/runtime/pages/llms-full.txt.ts +2 -2
  39. package/src/runtime/pages/llms.txt.ts +2 -2
  40. package/src/runtime/pages/markdown.ts +2 -2
  41. package/src/runtime/pages/search-index.ts +12 -3
  42. package/CHANGELOG.md +0 -35
  43. package/src/css/overrides.css +0 -1
  44. package/src/css/sources.css +0 -1
  45. package/src/runtime/lib/markdown-exporters/index.js +0 -144
  46. package/src/runtime/lib/mdx-source.js +0 -19
  47. package/src/runtime/lib/preview-source-plugin.js +0 -33
  48. package/src/runtime/scripts/copy-code.js +0 -130
  49. package/src/site/ContentHeader.astro +0 -33
package/README.md CHANGED
@@ -2,14 +2,14 @@
2
2
 
3
3
  A simple documentation system for Astro, built with **Basecoat** and **Tailwind CSS**.
4
4
 
5
- ReallySimpleDocs gives you a shadcn/ui-compatible docs template without a React runtime. It uses Basecoat for the same design language and CSS-variable model, while staying close to plain Astro, Markdown, HTML, CSS, and a small amount of vanilla JavaScript.
6
-
7
- Docs stay portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
5
+ ReallySimpleDocs keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
8
6
 
9
7
  ## Add to an Astro site
10
8
 
9
+ ReallySimpleDocs is an Astro integration. Add it to an existing Astro site, or create one first with `npm create astro@latest`.
10
+
11
11
  ```bash
12
- npm install reallysimpledocs@beta basecoat-css@beta tailwindcss
12
+ npm install reallysimpledocs basecoat-css tailwindcss
13
13
  ```
14
14
 
15
15
  Add the integration:
@@ -63,47 +63,39 @@ Welcome to the docs.
63
63
 
64
64
  ReallySimpleDocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
65
65
 
66
- Use `.mdx` when a page needs ReallySimpleDocs components. Common ReallySimpleDocs components are available by default, so you do not need to import them in each page:
67
-
68
- ```mdx
69
- # Button
66
+ Use component overrides for layout chrome such as sidebar branding, extra head tags, or header controls between search and the built-in theme toggle:
70
67
 
71
- <Callout title="MDX works">MDX is optional. Plain Markdown stays the default.</Callout>
68
+ ```js
69
+ reallySimpleDocs({
70
+ components: {
71
+ ContentHeader: "./src/components/ContentHeader.astro",
72
+ },
73
+ });
72
74
  ```
73
75
 
74
- ## Use Basecoat on the rest of the site
76
+ For example, put only the GitHub button in `ContentHeader`; the theme toggle stays built in.
75
77
 
76
- ReallySimpleDocs styles and scripts its generated docs pages. For custom Astro pages outside the docs route, such as a landing page or app page, use Basecoat directly in your own layout.
78
+ Use `.mdx` when a page needs ReallySimpleDocs components. Common components are available by default, so you do not need to import them in each page:
77
79
 
78
- Install stays the same:
80
+ ```mdx
81
+ # Button
79
82
 
80
- ```bash
81
- npm install reallysimpledocs@beta basecoat-css@beta tailwindcss
83
+ <Callout title="MDX works">MDX is optional. Plain Markdown stays the default.</Callout>
82
84
  ```
83
85
 
84
- Create a site stylesheet:
85
-
86
- ```css
87
- /* src/styles/site.css */
88
- @import "tailwindcss";
89
- @import "basecoat-css/base";
90
- @import "basecoat-css/styles/vega";
86
+ ## Use Basecoat outside the docs
91
87
 
92
- @source "../src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}";
93
- ```
88
+ By default, ReallySimpleDocs manages one Tailwind/Basecoat stylesheet for the app. It scans `src/`, `docs/`, and the ReallySimpleDocs runtime, so Basecoat and Tailwind classes used by custom Astro pages outside the docs route are included in the same CSS build.
94
89
 
95
- Load that stylesheet and Basecoat's JavaScript in your custom layout:
90
+ Disable managed CSS only when you want to provide the full stylesheet pipeline yourself:
96
91
 
97
- ```astro
98
- ---
99
- import "../styles/site.css";
100
- import "basecoat-css/all.min";
101
- ---
102
-
103
- <slot />
92
+ ```js
93
+ reallySimpleDocs({
94
+ css: false,
95
+ });
104
96
  ```
105
97
 
106
- Use the same Basecoat `style` in your ReallySimpleDocs config and your site stylesheet if you want the docs and custom pages to match.
98
+ Use `css: false` when you bring your own Tailwind/Basecoat/RSD stylesheet. ReallySimpleDocs still provides its managed JavaScript.
107
99
 
108
100
  ## Local development (this repo)
109
101
 
@@ -112,17 +104,25 @@ npm install
112
104
  npm run dev
113
105
  ```
114
106
 
107
+ Useful checks before publishing changes:
108
+
109
+ ```bash
110
+ npm run build
111
+ npm pack --dry-run
112
+ npm run smoke:consumer
113
+ ```
114
+
115
115
  ## Documentation
116
116
 
117
117
  Go to [ReallySimpleDocs.com](https://reallysimpledocs.com).
118
118
 
119
- ## Support the project
119
+ ## Support the project
120
120
 
121
- - [Contribute code](https://github.com/hunvreus/reallysimpledocs/blob/main/CONTRIBUTING.md)
121
+ - [Contribute code](/CONTRIBUTING.md)
122
122
  - [Report issues](https://github.com/hunvreus/reallysimpledocs/issues)
123
123
  - [Sponsor me](https://github.com/sponsors/hunvreus)
124
124
  - [Star the project on GitHub](https://github.com/hunvreus/reallysimpledocs)
125
125
 
126
126
  ## License
127
127
 
128
- [MIT](https://github.com/hunvreus/reallysimpledocs/blob/main/LICENSE.md)
128
+ [MIT](/LICENSE.md)
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "reallysimpledocs",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.js",
7
7
  "./astro": "./src/astro/index.js",
8
- "./components": "./src/components/index.js"
8
+ "./components": "./src/components/index.js",
9
+ "./css": "./src/css/custom.css",
10
+ "./css/styles/*": "./src/css/styles/*.css",
11
+ "./css/styles/*.css": "./src/css/styles/*.css"
9
12
  },
10
13
  "files": [
11
14
  "src",
12
15
  "README.md",
13
- "CHANGELOG.md",
14
16
  "LICENSE.md"
15
17
  ],
16
18
  "scripts": {
@@ -24,25 +26,20 @@
24
26
  "@astrojs/markdown-remark": "^7.2.0",
25
27
  "@astrojs/mdx": "^6.0.3",
26
28
  "@tailwindcss/vite": "4.1.17",
27
- "astro": "^6.4.6",
28
29
  "github-slugger": "^2.0.0",
29
30
  "lucide-static": "^0.552.0",
30
31
  "lunr": "^2.3.9",
31
32
  "marked": "^16.4.1",
32
- "mdast-util-gfm": "^3.1.0",
33
- "mdast-util-to-markdown": "^2.1.2",
34
- "remark-gfm": "^4.0.1",
35
- "remark-mdx": "^3.1.1",
36
- "remark-parse": "^11.0.0",
37
- "shiki": "^4.1.0",
38
- "unified": "^11.0.5"
33
+ "shiki": "^4.1.0"
39
34
  },
40
35
  "peerDependencies": {
41
- "basecoat-css": "^1.0.0-beta.2",
36
+ "astro": "^6.4.6",
37
+ "basecoat-css": "^1.0.0-beta.5",
42
38
  "tailwindcss": ">=4.1.17 <5"
43
39
  },
44
40
  "devDependencies": {
45
- "basecoat-css": "1.0.0-beta.2",
41
+ "astro": "^6.4.6",
42
+ "basecoat-css": "^1.0.0-beta.5",
46
43
  "tailwindcss": "4.1.17"
47
44
  },
48
45
  "overrides": {
@@ -1,10 +1,10 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { unified as astroMarkdown } from "@astrojs/markdown-remark";
5
4
  import mdx from "@astrojs/mdx";
5
+ import { unified } from "@astrojs/markdown-remark";
6
6
  import tailwindcss from "@tailwindcss/vite";
7
- import { previewSourceRemarkPlugin } from "../runtime/lib/preview-source-plugin.js";
7
+ import { remarkPreviewSource } from "./preview-source.js";
8
8
 
9
9
  const basecoatStyles = new Set(["vega", "nova", "maia", "lyra", "mira", "luma", "sera", "rhea"]);
10
10
 
@@ -28,12 +28,7 @@ const normalizeArray = (value) => (Array.isArray(value) ? value : value ? [value
28
28
 
29
29
  const normalizeImportPath = (filePath) => filePath.replaceAll(path.sep, "/");
30
30
 
31
- const normalizeRecord = (value) => (value && typeof value === "object" && !Array.isArray(value) ? value : {});
32
-
33
- const isInsideDir = (dir, file) => {
34
- const relative = path.relative(dir, file);
35
- return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
36
- };
31
+ const normalizeBoolean = (value, fallback = true) => (value === undefined ? fallback : Boolean(value));
37
32
 
38
33
  const shikiConfig = {
39
34
  themes: {
@@ -63,18 +58,6 @@ function collectMdxModules(docsDir) {
63
58
  return modules;
64
59
  }
65
60
 
66
- function componentModuleSource(modules) {
67
- const entries = Object.entries(modules);
68
- const imports = entries
69
- .map(([name, moduleId], index) => `import component${index} from ${JSON.stringify(moduleId)};`)
70
- .join("\n");
71
- return `${imports}
72
- export default {
73
- ${entries.map(([name], index) => ` ${JSON.stringify(name)}: component${index},`).join("\n")}
74
- };
75
- `;
76
- }
77
-
78
61
  export default function reallySimpleDocs(options = {}) {
79
62
  const style = options.style || "vega";
80
63
  if (!basecoatStyles.has(style)) {
@@ -87,9 +70,8 @@ export default function reallySimpleDocs(options = {}) {
87
70
  siteFile: options.siteFile || null,
88
71
  style,
89
72
  customCss: normalizeArray(options.customCss),
73
+ css: normalizeBoolean(options.css),
90
74
  components: options.components || {},
91
- mdxComponents: normalizeRecord(options.mdxComponents),
92
- markdownSerializers: normalizeRecord(options.markdownSerializers),
93
75
  routeBase: options.routeBase ?? "/docs",
94
76
  assetsBase: options.assetsBase || "/assets",
95
77
  };
@@ -97,57 +79,51 @@ export default function reallySimpleDocs(options = {}) {
97
79
  return {
98
80
  name: "reallysimpledocs",
99
81
  hooks: {
100
- "astro:config:setup": ({ injectRoute, updateConfig, config }) => {
82
+ "astro:config:setup": ({ injectRoute, injectScript, updateConfig, config }) => {
101
83
  const root = fileURLToPath(config.root);
102
84
  const generatedDir = path.join(root, ".astro", "reallysimpledocs");
103
85
  const generatedStyles = path.join(generatedDir, "styles.css");
104
86
  const docsDir = path.resolve(root, normalizedOptions.docsDir);
105
- const resolveUserId = (id) => (id?.startsWith(".") ? path.resolve(root, id) : id);
106
- const markdownSerializers = Object.fromEntries(
107
- Object.entries(normalizedOptions.markdownSerializers).map(([name, moduleId]) => [
108
- name,
109
- normalizeImportPath(resolveUserId(moduleId)),
110
- ]),
111
- );
112
- const mdxComponents = Object.fromEntries(
113
- Object.entries(normalizedOptions.mdxComponents).map(([name, moduleId]) => [
114
- name,
115
- normalizeImportPath(resolveUserId(moduleId)),
116
- ]),
117
- );
118
87
  const virtualConfig = {
119
88
  ...normalizedOptions,
120
89
  root,
121
90
  docsDir,
122
91
  siteFile: normalizedOptions.siteFile ? path.resolve(root, normalizedOptions.siteFile) : null,
123
- mdxComponents,
124
- markdownSerializers,
125
92
  routeBase: normalizeRouteBase(normalizedOptions.routeBase),
126
93
  };
127
- const components = {
128
- ContentHeader:
129
- normalizedOptions.components.ContentHeader || runtimePath("components/DefaultContentHeader.astro"),
130
- SidebarHeader:
131
- normalizedOptions.components.SidebarHeader || runtimePath("components/DefaultSidebarHeader.astro"),
132
- SidebarFooter:
133
- normalizedOptions.components.SidebarFooter || runtimePath("components/DefaultSidebarFooter.astro"),
94
+ const mdxModules = collectMdxModules(docsDir);
95
+ const resolveUserId = (id) => (id?.startsWith(".") ? path.resolve(root, id) : id);
96
+ const componentDefaults = {
97
+ Head: runtimePath("components/DefaultHeadContent.astro"),
98
+ SidebarHeader: runtimePath("components/DefaultSidebarHeader.astro"),
99
+ SidebarFooter: runtimePath("components/DefaultSidebarFooter.astro"),
100
+ ContentHeader: runtimePath("components/DefaultContentHeader.astro"),
134
101
  };
135
- fs.mkdirSync(generatedDir, { recursive: true });
136
- fs.writeFileSync(
137
- generatedStyles,
138
- [
139
- '@import "tailwindcss";',
140
- '@import "basecoat-css/base";',
141
- `@import "basecoat-css/styles/${style}";`,
142
- `@import ${JSON.stringify(cssPath("sources.css"))};`,
143
- `@import ${JSON.stringify(cssPath("custom.css"))};`,
144
- `@import ${JSON.stringify(cssPath("overrides.css"))};`,
145
- ...normalizedOptions.customCss.map((css) => `@import ${JSON.stringify(resolveUserId(css))};`),
146
- `@source ${JSON.stringify(runtimePath("**/*.{astro,js,ts}"))};`,
147
- `@source ${JSON.stringify(path.join(virtualConfig.docsDir, "**/*.{md,mdx}"))};`,
148
- "",
149
- ].join("\n"),
102
+ const components = Object.fromEntries(
103
+ Object.entries(componentDefaults).map(([name, defaultPath]) => [
104
+ name,
105
+ normalizedOptions.components[name] || defaultPath,
106
+ ]),
150
107
  );
108
+ fs.mkdirSync(generatedDir, { recursive: true });
109
+ const styleSource = normalizedOptions.css
110
+ ? [
111
+ '@import "tailwindcss";',
112
+ '@import "basecoat-css/base";',
113
+ `@import "basecoat-css/styles/${style}";`,
114
+ `@import ${JSON.stringify(cssPath("custom.css"))};`,
115
+ `@import ${JSON.stringify(cssPath(`styles/${style}.css`))};`,
116
+ ...normalizedOptions.customCss.map((css) => `@import ${JSON.stringify(resolveUserId(css))};`),
117
+ `@source ${JSON.stringify(path.join(root, "src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"))};`,
118
+ `@source ${JSON.stringify(runtimePath("**/*.{astro,js,ts}"))};`,
119
+ `@source ${JSON.stringify(path.join(virtualConfig.docsDir, "**/*.{md,mdx}"))};`,
120
+ "",
121
+ ].join("\n")
122
+ : "/* ReallySimpleDocs CSS disabled by config. */\n";
123
+ fs.writeFileSync(generatedStyles, styleSource);
124
+ if (normalizedOptions.css) {
125
+ injectScript("page-ssr", 'import "virtual:reallysimpledocs/styles.css";');
126
+ }
151
127
 
152
128
  injectRoute({
153
129
  pattern: routePattern(normalizedOptions.routeBase),
@@ -178,8 +154,8 @@ export default function reallySimpleDocs(options = {}) {
178
154
  }),
179
155
  ],
180
156
  markdown: {
181
- processor: astroMarkdown({
182
- remarkPlugins: [previewSourceRemarkPlugin],
157
+ processor: unified({
158
+ remarkPlugins: [[remarkPreviewSource, { docsDir }]],
183
159
  }),
184
160
  shikiConfig,
185
161
  },
@@ -191,41 +167,20 @@ export default function reallySimpleDocs(options = {}) {
191
167
  tailwindcss(),
192
168
  {
193
169
  name: "reallysimpledocs-virtual-modules",
194
- configureServer(server) {
195
- const invalidateMdxPages = () => {
196
- const module = server.moduleGraph.getModuleById("\0virtual:reallysimpledocs/mdx-pages");
197
- if (module) server.moduleGraph.invalidateModule(module);
198
- server.ws.send({ type: "full-reload" });
199
- };
200
-
201
- server.watcher.add(path.join(docsDir, "**/*.mdx"));
202
- server.watcher.on("add", (file) => {
203
- if (file.endsWith(".mdx") && isInsideDir(docsDir, file)) invalidateMdxPages();
204
- });
205
- server.watcher.on("unlink", (file) => {
206
- if (file.endsWith(".mdx") && isInsideDir(docsDir, file)) invalidateMdxPages();
207
- });
208
- },
209
170
  resolveId(id) {
210
171
  if (id === "virtual:reallysimpledocs/config") return "\0virtual:reallysimpledocs/config";
211
172
  if (id === "virtual:reallysimpledocs/mdx-pages") return "\0virtual:reallysimpledocs/mdx-pages";
212
- if (id === "virtual:reallysimpledocs/mdx-components") return "\0virtual:reallysimpledocs/mdx-components";
213
173
  if (id === "virtual:reallysimpledocs/styles.css") return generatedStyles;
214
- if (id === "virtual:reallysimpledocs/components/SidebarHeader") {
215
- return "\0virtual:reallysimpledocs/components/SidebarHeader";
216
- }
217
- if (id === "virtual:reallysimpledocs/components/SidebarFooter") {
218
- return "\0virtual:reallysimpledocs/components/SidebarFooter";
219
- }
220
- if (id === "virtual:reallysimpledocs/components/ContentHeader") {
221
- return "\0virtual:reallysimpledocs/components/ContentHeader";
174
+ for (const name of Object.keys(components)) {
175
+ if (id === `virtual:reallysimpledocs/components/${name}`) {
176
+ return `\0virtual:reallysimpledocs/components/${name}`;
177
+ }
222
178
  }
223
179
  return null;
224
180
  },
225
181
  load(id) {
226
182
  if (id === "\0virtual:reallysimpledocs/config") return `export default ${JSON.stringify(virtualConfig)};`;
227
183
  if (id === "\0virtual:reallysimpledocs/mdx-pages") {
228
- const mdxModules = collectMdxModules(docsDir);
229
184
  const imports = Object.entries(mdxModules)
230
185
  .map(([slug, filePath], index) => `import * as mdx${index} from ${JSON.stringify(filePath)};`)
231
186
  .join("\n");
@@ -241,17 +196,10 @@ export async function getMdxPage(slug) {
241
196
  }
242
197
  `;
243
198
  }
244
- if (id === "\0virtual:reallysimpledocs/mdx-components") {
245
- return componentModuleSource(mdxComponents);
246
- }
247
- if (id === "\0virtual:reallysimpledocs/components/SidebarHeader") {
248
- return `export { default } from ${JSON.stringify(resolveUserId(components.SidebarHeader))};`;
249
- }
250
- if (id === "\0virtual:reallysimpledocs/components/SidebarFooter") {
251
- return `export { default } from ${JSON.stringify(resolveUserId(components.SidebarFooter))};`;
252
- }
253
- if (id === "\0virtual:reallysimpledocs/components/ContentHeader") {
254
- return `export { default } from ${JSON.stringify(resolveUserId(components.ContentHeader))};`;
199
+ for (const [name, componentPath] of Object.entries(components)) {
200
+ if (id === `\0virtual:reallysimpledocs/components/${name}`) {
201
+ return `export { default } from ${JSON.stringify(resolveUserId(componentPath))};`;
202
+ }
255
203
  }
256
204
  return null;
257
205
  },
@@ -0,0 +1,90 @@
1
+ import { toMarkdown } from "mdast-util-to-markdown";
2
+ import { mdxToMarkdown } from "mdast-util-mdx";
3
+
4
+ export function remarkPreviewSource({ docsDir } = {}) {
5
+ const normalizedDocsDir = docsDir ? normalizePath(docsDir).replace(/\/$/, "") : "";
6
+
7
+ return (tree, file) => {
8
+ if (normalizedDocsDir && !isDocsFile(file?.path, normalizedDocsDir)) return;
9
+
10
+ visit(tree, (node) => {
11
+ if (node?.type !== "mdxJsxFlowElement") return;
12
+ if (node.name === "Preview") addPreviewSource(node);
13
+ if (node.name === "Tabs") addTabsMetadata(node);
14
+ });
15
+ };
16
+ }
17
+
18
+ function isDocsFile(filePath, docsDir) {
19
+ const normalized = normalizePath(filePath || "");
20
+ return normalized === docsDir || normalized.startsWith(`${docsDir}/`);
21
+ }
22
+
23
+ function normalizePath(value) {
24
+ return String(value || "").replaceAll("\\", "/");
25
+ }
26
+
27
+ function addPreviewSource(node) {
28
+ node.attributes ||= [];
29
+ if (hasAttr(node, "code")) return;
30
+
31
+ const source = toMarkdown({ type: "root", children: node.children || [] }, { extensions: [mdxToMarkdown()] }).trim();
32
+ node.attributes.push({ type: "mdxJsxAttribute", name: "code", value: source });
33
+ }
34
+
35
+ function addTabsMetadata(node) {
36
+ node.attributes ||= [];
37
+ if (hasAttr(node, "tabs")) return;
38
+
39
+ const tabNodes = (node.children || []).filter((child) => child?.type === "mdxJsxFlowElement" && child.name === "Tab");
40
+ if (!tabNodes.length) return;
41
+
42
+ const seen = new Map();
43
+ const tabs = tabNodes.map((tab, index) => {
44
+ tab.attributes ||= [];
45
+ const title = getAttr(tab, "title") || `Tab ${index + 1}`;
46
+ const value = uniqueValue(slugify(title) || `tab-${index + 1}`, seen);
47
+ if (!hasAttr(tab, "slot")) {
48
+ tab.attributes.push({ type: "mdxJsxAttribute", name: "slot", value });
49
+ }
50
+ return {
51
+ label: title,
52
+ slot: value,
53
+ };
54
+ });
55
+
56
+ node.attributes.push({
57
+ type: "mdxJsxAttribute",
58
+ name: "tabs",
59
+ value: JSON.stringify(tabs),
60
+ });
61
+ }
62
+
63
+ function visit(node, visitor) {
64
+ visitor(node);
65
+ if (!Array.isArray(node?.children)) return;
66
+ for (const child of node.children) visit(child, visitor);
67
+ }
68
+
69
+ function getAttr(node, name) {
70
+ const attr = (node.attributes || []).find((item) => item.type === "mdxJsxAttribute" && item.name === name);
71
+ return typeof attr?.value === "string" ? attr.value : "";
72
+ }
73
+
74
+ function hasAttr(node, name) {
75
+ return (node.attributes || []).some((item) => item.type === "mdxJsxAttribute" && item.name === name);
76
+ }
77
+
78
+ function slugify(value) {
79
+ return String(value || "")
80
+ .trim()
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9]+/g, "-")
83
+ .replace(/^-+|-+$/g, "");
84
+ }
85
+
86
+ function uniqueValue(value, seen) {
87
+ const count = seen.get(value) || 0;
88
+ seen.set(value, count + 1);
89
+ return count ? `${value}-${count + 1}` : value;
90
+ }
@@ -10,4 +10,5 @@ export { default as CodeGroup } from "../runtime/components/mdx/CodeGroup.astro"
10
10
  export { default as Preview } from "../runtime/components/mdx/Preview.astro";
11
11
  export { default as Step } from "../runtime/components/mdx/Step.astro";
12
12
  export { default as Steps } from "../runtime/components/mdx/Steps.astro";
13
+ export { default as Tab } from "../runtime/components/mdx/Tab.astro";
13
14
  export { default as Tabs } from "../runtime/components/mdx/Tabs.astro";