reallysimpledocs 1.0.0-beta.3 → 1.0.0-beta.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 ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ ## [1.0.0-beta.4] - 2026-06-16
8
+
9
+ ### Added
10
+
11
+ - Added MDX documentation components for callouts, code blocks, code groups, previews, tabs, steps, and cards.
12
+ - Added automatic MDX component availability in docs pages, so common ReallySimpleDocs components do not need page-level imports.
13
+ - Added custom `mdxComponents` registration for project-specific MDX components.
14
+ - Added custom `markdownSerializers` registration so project-specific MDX components can control Markdown, search, and LLM export output.
15
+ - Added Markdown export serializers for ReallySimpleDocs components so generated Markdown, search content, and `llms-full.txt` avoid raw MDX component noise.
16
+ - Added customizable sidebar header, sidebar footer, and content header component overrides.
17
+ - Added breadcrumb rendering from the docs navigation tree.
18
+ - Added Lunr-powered command search with result previews and highlighted matches.
19
+ - Added Basecoat style selection through the ReallySimpleDocs integration config.
20
+
21
+ ### Changed
22
+
23
+ - Ported ReallySimpleDocs to an Astro integration with generated docs routes, assets, Markdown exports, search index, and Basecoat styling.
24
+ - Updated docs content to use Markdown by default and MDX only for richer component examples.
25
+ - Updated code examples to use inline component children instead of passing source code as props.
26
+ - Changed empty table-of-contents behavior so the right panel is hidden instead of rendering an empty “On this page” section.
27
+ - Changed breadcrumbs to render in the sticky content header for nested pages.
28
+ - Updated the docs overview and component pages to explain component usage, Markdown output, and Basecoat interoperability.
29
+
30
+ ### Fixed
31
+
32
+ - Fixed Shiki code block colors across light and dark mode.
33
+ - Fixed copy-code button layout for regular code blocks and tabbed code blocks.
34
+ - Fixed callout rendering in Markdown and MDX pages.
35
+ - Fixed sidebar and header markup to follow Basecoat component patterns.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  A simple documentation system for Astro, built with **Basecoat** and **Tailwind CSS**.
4
4
 
5
- reallysimpledocs keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
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.
6
8
 
7
9
  ## Add to an Astro site
8
10
 
@@ -59,9 +61,9 @@ docs/
59
61
  Welcome to the docs.
60
62
  ```
61
63
 
62
- reallysimpledocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
64
+ ReallySimpleDocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
63
65
 
64
- 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:
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:
65
67
 
66
68
  ```mdx
67
69
  # Button
@@ -71,7 +73,7 @@ Use `.mdx` when a page needs reallysimpledocs components. Common reallysimpledoc
71
73
 
72
74
  ## Use Basecoat on the rest of the site
73
75
 
74
- reallysimpledocs styles and scripts its generated docs pages. For custom Astro pages outside the docs route, use Basecoat directly in your own layout.
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.
75
77
 
76
78
  Install stays the same:
77
79
 
@@ -101,7 +103,7 @@ import "basecoat-css/all.min";
101
103
  <slot />
102
104
  ```
103
105
 
104
- Use the same Basecoat `style` in your reallysimpledocs config and your site stylesheet if you want the docs and custom pages to match.
106
+ Use the same Basecoat `style` in your ReallySimpleDocs config and your site stylesheet if you want the docs and custom pages to match.
105
107
 
106
108
  ## Local development (this repo)
107
109
 
@@ -114,13 +116,13 @@ npm run dev
114
116
 
115
117
  Go to [ReallySimpleDocs.com](https://reallysimpledocs.com).
116
118
 
117
- ## Support the project
119
+ ## Support the project
118
120
 
119
- - [Contribute code](/CONTRIBUTING.md)
121
+ - [Contribute code](https://github.com/hunvreus/reallysimpledocs/blob/main/CONTRIBUTING.md)
120
122
  - [Report issues](https://github.com/hunvreus/reallysimpledocs/issues)
121
123
  - [Sponsor me](https://github.com/sponsors/hunvreus)
122
124
  - [Star the project on GitHub](https://github.com/hunvreus/reallysimpledocs)
123
125
 
124
126
  ## License
125
127
 
126
- [MIT](/LICENSE.md)
128
+ [MIT](https://github.com/hunvreus/reallysimpledocs/blob/main/LICENSE.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallysimpledocs",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.4",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.js",
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "src",
12
12
  "README.md",
13
+ "CHANGELOG.md",
13
14
  "LICENSE.md"
14
15
  ],
15
16
  "scripts": {
@@ -20,6 +21,7 @@
20
21
  "check": "npm run build && npm pack --dry-run && npm run smoke:consumer"
21
22
  },
22
23
  "dependencies": {
24
+ "@astrojs/markdown-remark": "^7.2.0",
23
25
  "@astrojs/mdx": "^6.0.3",
24
26
  "@tailwindcss/vite": "4.1.17",
25
27
  "astro": "^6.4.6",
@@ -27,7 +29,13 @@
27
29
  "lucide-static": "^0.552.0",
28
30
  "lunr": "^2.3.9",
29
31
  "marked": "^16.4.1",
30
- "shiki": "^4.1.0"
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"
31
39
  },
32
40
  "peerDependencies": {
33
41
  "basecoat-css": "^1.0.0-beta.2",
@@ -1,8 +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";
4
5
  import mdx from "@astrojs/mdx";
5
6
  import tailwindcss from "@tailwindcss/vite";
7
+ import { previewSourceRemarkPlugin } from "../runtime/lib/preview-source-plugin.js";
6
8
 
7
9
  const basecoatStyles = new Set(["vega", "nova", "maia", "lyra", "mira", "luma", "sera", "rhea"]);
8
10
 
@@ -26,6 +28,8 @@ const normalizeArray = (value) => (Array.isArray(value) ? value : value ? [value
26
28
 
27
29
  const normalizeImportPath = (filePath) => filePath.replaceAll(path.sep, "/");
28
30
 
31
+ const normalizeRecord = (value) => (value && typeof value === "object" && !Array.isArray(value) ? value : {});
32
+
29
33
  const isInsideDir = (dir, file) => {
30
34
  const relative = path.relative(dir, file);
31
35
  return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
@@ -59,6 +63,18 @@ function collectMdxModules(docsDir) {
59
63
  return modules;
60
64
  }
61
65
 
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
+
62
78
  export default function reallySimpleDocs(options = {}) {
63
79
  const style = options.style || "vega";
64
80
  if (!basecoatStyles.has(style)) {
@@ -72,6 +88,8 @@ export default function reallySimpleDocs(options = {}) {
72
88
  style,
73
89
  customCss: normalizeArray(options.customCss),
74
90
  components: options.components || {},
91
+ mdxComponents: normalizeRecord(options.mdxComponents),
92
+ markdownSerializers: normalizeRecord(options.markdownSerializers),
75
93
  routeBase: options.routeBase ?? "/docs",
76
94
  assetsBase: options.assetsBase || "/assets",
77
95
  };
@@ -84,17 +102,35 @@ export default function reallySimpleDocs(options = {}) {
84
102
  const generatedDir = path.join(root, ".astro", "reallysimpledocs");
85
103
  const generatedStyles = path.join(generatedDir, "styles.css");
86
104
  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
+ );
87
118
  const virtualConfig = {
88
119
  ...normalizedOptions,
89
120
  root,
90
121
  docsDir,
91
122
  siteFile: normalizedOptions.siteFile ? path.resolve(root, normalizedOptions.siteFile) : null,
123
+ mdxComponents,
124
+ markdownSerializers,
92
125
  routeBase: normalizeRouteBase(normalizedOptions.routeBase),
93
126
  };
94
- const resolveUserId = (id) => (id?.startsWith(".") ? path.resolve(root, id) : id);
95
127
  const components = {
128
+ ContentHeader:
129
+ normalizedOptions.components.ContentHeader || runtimePath("components/DefaultContentHeader.astro"),
96
130
  SidebarHeader:
97
131
  normalizedOptions.components.SidebarHeader || runtimePath("components/DefaultSidebarHeader.astro"),
132
+ SidebarFooter:
133
+ normalizedOptions.components.SidebarFooter || runtimePath("components/DefaultSidebarFooter.astro"),
98
134
  };
99
135
  fs.mkdirSync(generatedDir, { recursive: true });
100
136
  fs.writeFileSync(
@@ -142,6 +178,9 @@ export default function reallySimpleDocs(options = {}) {
142
178
  }),
143
179
  ],
144
180
  markdown: {
181
+ processor: astroMarkdown({
182
+ remarkPlugins: [previewSourceRemarkPlugin],
183
+ }),
145
184
  shikiConfig,
146
185
  },
147
186
  vite: {
@@ -170,10 +209,17 @@ export default function reallySimpleDocs(options = {}) {
170
209
  resolveId(id) {
171
210
  if (id === "virtual:reallysimpledocs/config") return "\0virtual:reallysimpledocs/config";
172
211
  if (id === "virtual:reallysimpledocs/mdx-pages") return "\0virtual:reallysimpledocs/mdx-pages";
212
+ if (id === "virtual:reallysimpledocs/mdx-components") return "\0virtual:reallysimpledocs/mdx-components";
173
213
  if (id === "virtual:reallysimpledocs/styles.css") return generatedStyles;
174
214
  if (id === "virtual:reallysimpledocs/components/SidebarHeader") {
175
215
  return "\0virtual:reallysimpledocs/components/SidebarHeader";
176
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";
222
+ }
177
223
  return null;
178
224
  },
179
225
  load(id) {
@@ -195,9 +241,18 @@ export async function getMdxPage(slug) {
195
241
  }
196
242
  `;
197
243
  }
244
+ if (id === "\0virtual:reallysimpledocs/mdx-components") {
245
+ return componentModuleSource(mdxComponents);
246
+ }
198
247
  if (id === "\0virtual:reallysimpledocs/components/SidebarHeader") {
199
248
  return `export { default } from ${JSON.stringify(resolveUserId(components.SidebarHeader))};`;
200
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))};`;
255
+ }
201
256
  return null;
202
257
  },
203
258
  },
@@ -23,37 +23,40 @@
23
23
  > h1:first-child {
24
24
  @apply hidden;
25
25
  }
26
- p > a,
27
- li > a,
28
- dt > a,
29
- dd > a,
30
- td > a,
31
- th > a {
26
+ > p > a,
27
+ > blockquote a,
28
+ > :is(ol, ul) a,
29
+ > dl a,
30
+ > table a {
32
31
  @apply underline underline-offset-2 decoration-muted-foreground/50 hover:decoration-foreground/50 transition-all;
33
32
  }
34
- code:not(pre > code) {
33
+ > p code:not(pre > code),
34
+ > blockquote code:not(pre > code),
35
+ > :is(ol, ul) code:not(pre > code),
36
+ > dl code:not(pre > code),
37
+ > table code:not(pre > code) {
35
38
  @apply relative rounded-md bg-muted px-[0.3rem] py-[0.2rem] font-mono text-[13px];
36
39
  }
37
- pre {
40
+ > pre {
38
41
  @apply rounded-lg;
39
42
  }
40
- pre > code {
43
+ > pre > code {
41
44
  @apply block min-w-full w-max px-4 py-3 text-[13px];
42
45
  }
43
- pre:has(> code) {
46
+ > pre:has(> code) {
44
47
  @apply max-w-full overflow-x-auto;
45
48
  }
46
- blockquote {
49
+ > blockquote {
47
50
  @apply border-l-4 border-border pl-4;
48
51
  }
49
- ol {
52
+ > ol {
50
53
  @apply list-decimal ml-6;
51
54
  }
52
- ul {
55
+ > ul {
53
56
  @apply list-disc ml-6;
54
57
  }
55
- ol,
56
- ul {
58
+ > ol,
59
+ > ul {
57
60
  li {
58
61
  @apply mt-2;
59
62
 
@@ -63,7 +66,7 @@
63
66
  }
64
67
  }
65
68
  }
66
- dl {
69
+ > dl {
67
70
  @apply border-b pb-3 my-6;
68
71
  dt {
69
72
  @apply border-t pt-3 mt-3 first:mt-0;
@@ -75,16 +78,24 @@
75
78
  @apply mt-4 border-b-0 pb-0 pl-8 my-0;
76
79
  }
77
80
  }
78
- b,
79
- strong {
81
+ > p b,
82
+ > p strong,
83
+ > blockquote b,
84
+ > blockquote strong,
85
+ > :is(ol, ul) b,
86
+ > :is(ol, ul) strong,
87
+ > dl b,
88
+ > dl strong,
89
+ > table b,
90
+ > table strong {
80
91
  @apply font-semibold;
81
92
  }
82
93
  > p,
83
- blockquote,
84
- ul,
85
- ol,
86
- dl,
87
- pre {
94
+ > blockquote,
95
+ > ul,
96
+ > ol,
97
+ > dl,
98
+ > pre {
88
99
  @apply my-6;
89
100
  }
90
101
  > *:first-child {
@@ -93,7 +104,7 @@
93
104
  > *:last-child {
94
105
  @apply !mb-0;
95
106
  }
96
- table {
107
+ > table {
97
108
  @apply w-full caption-bottom text-sm;
98
109
  thead {
99
110
  @apply [&_tr]:border-b;
@@ -16,7 +16,7 @@ const { indexUrl } = Astro.props;
16
16
  </div>
17
17
 
18
18
  <script>
19
- import lunr from "lunr";
19
+ import lunrUrl from "lunr/lunr.js?url";
20
20
 
21
21
  const root = document.querySelector("[data-search]");
22
22
  const indexUrl = root?.getAttribute("data-index-url");
@@ -26,6 +26,7 @@ const { indexUrl } = Astro.props;
26
26
  const menu = dialog?.querySelector('[role="menu"]');
27
27
  const state = {
28
28
  loadPromise: null,
29
+ lunrPromise: null,
29
30
  index: null,
30
31
  documents: [],
31
32
  bySlug: new Map(),
@@ -197,6 +198,21 @@ const { indexUrl } = Astro.props;
197
198
  refreshCommand();
198
199
  }
199
200
 
201
+ function loadLunr() {
202
+ if (window.lunr) return Promise.resolve(window.lunr);
203
+ if (!state.lunrPromise) {
204
+ state.lunrPromise = new Promise((resolve, reject) => {
205
+ const script = document.createElement("script");
206
+ script.src = lunrUrl;
207
+ script.async = true;
208
+ script.onload = () => (window.lunr ? resolve(window.lunr) : reject(new Error("Failed to load lunr.")));
209
+ script.onerror = () => reject(new Error("Failed to load lunr."));
210
+ document.head.appendChild(script);
211
+ });
212
+ }
213
+ return state.lunrPromise;
214
+ }
215
+
200
216
  async function loadIndex() {
201
217
  if (state.index) return state;
202
218
  if (!indexUrl) throw new Error("Missing reallysimpledocs search index URL.");
@@ -204,7 +220,8 @@ const { indexUrl } = Astro.props;
204
220
  renderLoading();
205
221
  state.loadPromise = fetch(indexUrl)
206
222
  .then((response) => response.json())
207
- .then((data) => {
223
+ .then(async (data) => {
224
+ const lunr = await loadLunr();
208
225
  state.documents = data.documents || [];
209
226
  state.bySlug = new Map(state.documents.map((doc) => [doc.slug, doc]));
210
227
  state.index = lunr.Index.load(data.index);
@@ -0,0 +1,19 @@
1
+ ---
2
+ import Moon from "lucide-static/icons/moon.svg?raw";
3
+ import Sun from "lucide-static/icons/sun.svg?raw";
4
+ ---
5
+
6
+ <nav class="hidden items-center gap-2 sm:flex">
7
+ <button
8
+ type="button"
9
+ aria-label="Toggle dark mode"
10
+ data-tooltip="Toggle dark mode"
11
+ data-side="bottom"
12
+ data-align="end"
13
+ onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
14
+ class="btn-sm-icon-ghost"
15
+ >
16
+ <span class="hidden dark:block"><Fragment set:html={Sun} /></span>
17
+ <span class="block dark:hidden"><Fragment set:html={Moon} /></span>
18
+ </button>
19
+ </nav>
@@ -4,12 +4,11 @@ import ArrowLeft from "lucide-static/icons/arrow-left.svg?raw";
4
4
  import ArrowRight from "lucide-static/icons/arrow-right.svg?raw";
5
5
  import Check from "lucide-static/icons/check.svg?raw";
6
6
  import Copy from "lucide-static/icons/copy.svg?raw";
7
- import Menu from "lucide-static/icons/menu.svg?raw";
8
- import Moon from "lucide-static/icons/moon.svg?raw";
9
7
  import PanelLeft from "lucide-static/icons/panel-left.svg?raw";
10
8
  import Search from "lucide-static/icons/search.svg?raw";
11
- import Sun from "lucide-static/icons/sun.svg?raw";
12
- import X from "lucide-static/icons/x.svg?raw";
9
+ import ChevronRight from "lucide-static/icons/chevron-right.svg?raw";
10
+ import Ellipsis from "lucide-static/icons/ellipsis.svg?raw";
11
+ import ContentHeader from "virtual:reallysimpledocs/components/ContentHeader";
13
12
  import ClaudeLogo from "../../icons/logos/claude.svg?raw";
14
13
  import CursorLogo from "../../icons/logos/cursor.svg?raw";
15
14
  import MarkdownLogo from "../../icons/logos/markdown.svg?raw";
@@ -18,18 +17,36 @@ import BaseLayout from "./BaseLayout.astro";
18
17
  import CommandDialog from "./CommandDialog.astro";
19
18
  import DropdownMenu from "./DropdownMenu.astro";
20
19
  import Sidebar from "./Sidebar.astro";
21
- import { buildMenus, getNavigation } from "../lib/docs.js";
20
+ import { buildMenus, getBreadcrumbs, getNavigation } from "../lib/docs.js";
22
21
  import { addSvgClass } from "../lib/html.js";
22
+ import { renderDropdownMenu } from "../lib/macros.js";
23
23
  import { routeFilePath } from "../lib/paths.js";
24
24
  import { getSite } from "../lib/site.js";
25
25
 
26
26
  const { page, headings } = Astro.props;
27
27
  const site = getSite(config);
28
28
  const navigation = getNavigation(config, page);
29
+ const breadcrumbs = getBreadcrumbs(config, page);
29
30
  const { sidebar } = buildMenus(config, page.slug);
31
+ const hasToc = headings.length > 0;
30
32
  const searchIndexUrl = routeFilePath(config.routeBase, "search-index.json");
33
+ const chevronRightIcon = addSvgClass(ChevronRight, "size-3.5");
34
+ const ellipsisIcon = addSvgClass(Ellipsis, "size-4");
31
35
  const panelLeftIcon = addSvgClass(PanelLeft, "size-4");
32
36
  const searchIcon = addSvgClass(Search, "size-4");
37
+ const breadcrumbItems =
38
+ breadcrumbs.length < 2
39
+ ? []
40
+ : breadcrumbs.length > 3
41
+ ? [
42
+ breadcrumbs[0],
43
+ {
44
+ type: "dropdown",
45
+ items: breadcrumbs.slice(1, -1),
46
+ },
47
+ breadcrumbs.at(-1),
48
+ ]
49
+ : breadcrumbs;
33
50
  const siteUrl = Astro.site?.toString().replace(/\/$/, "") || site.url || "";
34
51
  const pageUrl = `${siteUrl}${page.path}`;
35
52
  const llmPrompt = `Read from ${pageUrl} so I can ask questions about it.`;
@@ -84,74 +101,74 @@ const actionItems = [
84
101
  <Fragment set:html={panelLeftIcon} />
85
102
  </button>
86
103
 
104
+ {
105
+ breadcrumbItems.length > 0 && (
106
+ <nav class="hidden min-w-0 overflow-visible md:block" aria-label="Breadcrumb">
107
+ <ol class="flex min-w-0 items-center gap-1.5 overflow-visible text-sm text-muted-foreground">
108
+ {breadcrumbItems.map((crumb, index) => {
109
+ const current = index === breadcrumbItems.length - 1;
110
+ return (
111
+ <>
112
+ {index > 0 && (
113
+ <li class="shrink-0" aria-hidden="true">
114
+ <Fragment set:html={chevronRightIcon} />
115
+ </li>
116
+ )}
117
+ <li class="inline-flex min-w-0 items-center gap-1.5 overflow-visible">
118
+ {crumb.type === "dropdown" ? (
119
+ <Fragment
120
+ set:html={renderDropdownMenu({
121
+ id: `breadcrumb-${page.slug.replace(/[^a-z0-9_-]/gi, "-")}`,
122
+ trigger: ellipsisIcon,
123
+ items: crumb.items.map((item) => ({
124
+ label: item.label,
125
+ url: item.url,
126
+ })),
127
+ triggerAttrs: {
128
+ class: "flex size-4 items-center justify-center hover:text-foreground cursor-pointer",
129
+ "aria-label": "Show breadcrumb pages",
130
+ },
131
+ popoverAttrs: { "data-align": "start" },
132
+ })}
133
+ />
134
+ ) : current || !crumb.url ? (
135
+ <span class="truncate font-medium text-foreground">{crumb.label}</span>
136
+ ) : (
137
+ <a href={crumb.url} class="truncate transition-colors hover:text-foreground">
138
+ {crumb.label}
139
+ </a>
140
+ )}
141
+ </li>
142
+ </>
143
+ );
144
+ })}
145
+ </ol>
146
+ </nav>
147
+ )
148
+ }
149
+
87
150
  <button
88
151
  type="button"
89
- class="relative flex h-8 w-full min-w-0 max-w-72 cursor-text items-center gap-x-2 rounded-md border border-input bg-background px-3 py-1 text-base text-muted-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30 sm:ml-auto md:text-sm"
152
+ class="btn-sm-icon-ghost ml-auto sm:hidden"
153
+ aria-label="Search"
154
+ data-tooltip="Search"
155
+ data-side="bottom"
90
156
  onclick="window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal()"
91
157
  >
92
158
  <Fragment set:html={searchIcon} />
93
- Search the docs...
94
- <kbd class="kbd ml-auto -mr-1.5">⌘K</kbd>
95
159
  </button>
96
160
 
97
- <nav class="hidden items-center gap-2 sm:flex">
98
- {
99
- (site.links || []).map((link) => (
100
- <a
101
- class={link.attrs?.class || "btn-sm-outline"}
102
- href={link.url}
103
- aria-label={link.label}
104
- data-tooltip={link.label}
105
- data-side="bottom"
106
- target={link.attrs?.target}
107
- rel={link.attrs?.rel}
108
- >
109
- {link.icon && <Fragment set:html={link.icon} />}
110
- {!link.iconOnly && link.label}
111
- </a>
112
- ))
113
- }
114
- <button
115
- type="button"
116
- aria-label="Toggle dark mode"
117
- data-tooltip="Toggle dark mode"
118
- data-side="bottom"
119
- data-align="end"
120
- onclick="document.dispatchEvent(new CustomEvent('basecoat:theme'))"
121
- class="btn-sm-icon-outline"
122
- >
123
- <span class="hidden dark:block"><Fragment set:html={Sun} /></span>
124
- <span class="block dark:hidden"><Fragment set:html={Moon} /></span>
125
- </button>
126
- </nav>
161
+ <button
162
+ type="button"
163
+ class="relative hidden h-8 w-full min-w-0 max-w-72 cursor-text items-center gap-x-2 rounded-md border border-input bg-background px-3 py-1 text-base text-muted-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30 sm:ml-auto sm:flex md:text-sm"
164
+ onclick="window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal()"
165
+ >
166
+ <Fragment set:html={searchIcon} />
167
+ Search...
168
+ <kbd class="kbd ml-auto -mr-1.5">⌘K</kbd>
169
+ </button>
127
170
 
128
- <div class="sm:hidden">
129
- <button
130
- type="button"
131
- class="btn-sm-icon-ghost peer group"
132
- aria-label="Toggle menu"
133
- aria-expanded="false"
134
- aria-controls="mobile-menu"
135
- onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') === 'true' ? 'false' : 'true')"
136
- >
137
- <span class="group-aria-expanded:hidden"><Fragment set:html={Menu} /></span>
138
- <span class="hidden group-aria-expanded:block"><Fragment set:html={X} /></span>
139
- </button>
140
- <nav
141
- id="mobile-menu"
142
- class="fixed inset-0 top-14 z-50 flex invisible -translate-y-16 scale-95 flex-col gap-2 bg-background p-4 opacity-0 transition-all pointer-events-none peer-aria-expanded:visible peer-aria-expanded:translate-y-0 peer-aria-expanded:scale-100 peer-aria-expanded:opacity-100 peer-aria-expanded:pointer-events-auto"
143
- data-mobile-menu
144
- >
145
- {
146
- (site.links || []).map((link) => (
147
- <a class="btn-lg-ghost justify-between" href={link.url} target={link.attrs?.target} rel={link.attrs?.rel}>
148
- <span>{link.label}</span>
149
- {link.icon && <Fragment set:html={link.icon} />}
150
- </a>
151
- ))
152
- }
153
- </nav>
154
- </div>
171
+ <ContentHeader site={site} page={page} navigation={navigation} config={config} />
155
172
  </div>
156
173
  </header>
157
174
 
@@ -221,22 +238,24 @@ const actionItems = [
221
238
  </footer>
222
239
  </div>
223
240
  </article>
224
- <aside class="hidden w-56 xl:block">
225
- <div class="docs-toc sticky top-20 space-y-2 text-sm [&_a]:text-muted-foreground [&_a]:hover:text-foreground [&_a[data-active='true']]:text-foreground [&_ol]:space-y-2 [&_ol_ol]:mt-2 [&_ol_ol]:pl-3" data-docs-toc>
226
- <h2 class="flex items-center gap-1 font-medium">
227
- On this page
228
- </h2>
229
- <ol>
230
- {
231
- headings.map((heading) => (
232
- <li class={heading.depth === 3 ? "pl-3" : ""}>
233
- <a href={`#${heading.id}`} data-toc-link>{heading.text}</a>
234
- </li>
235
- ))
236
- }
237
- </ol>
238
- </div>
239
- </aside>
241
+ {
242
+ hasToc && (
243
+ <aside class="hidden w-56 xl:block">
244
+ <div class="docs-toc sticky top-20 space-y-2 text-sm [&_a]:text-muted-foreground [&_a]:hover:text-foreground [&_a[data-active='true']]:text-foreground [&_ol]:space-y-2 [&_ol_ol]:mt-2 [&_ol_ol]:pl-3" data-docs-toc>
245
+ <h2 class="flex items-center gap-1 font-medium text-muted-foreground">On this page</h2>
246
+ <ol>
247
+ {headings.map((heading) => (
248
+ <li class={heading.depth === 3 ? "pl-3" : ""}>
249
+ <a href={`#${heading.id}`} data-toc-link>
250
+ {heading.text}
251
+ </a>
252
+ </li>
253
+ ))}
254
+ </ol>
255
+ </div>
256
+ </aside>
257
+ )
258
+ }
240
259
  </div>
241
260
  </main>
242
261