reallysimpledocs 0.1.3

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.
@@ -0,0 +1,269 @@
1
+ ---
2
+ import { renderCommandDialog } from "../lib/macros.js";
3
+
4
+ const { indexUrl } = Astro.props;
5
+ ---
6
+
7
+ <div data-rsd-command-search data-index-url={indexUrl}>
8
+ <Fragment
9
+ set:html={renderCommandDialog({
10
+ id: "command-search",
11
+ items: [],
12
+ emptyText: "Type to search docs.",
13
+ commandAttrs: { "data-filter": "manual" },
14
+ })}
15
+ />
16
+ </div>
17
+
18
+ <script>
19
+ import lunr from "lunr";
20
+
21
+ const root = document.querySelector("[data-rsd-command-search]");
22
+ const indexUrl = root?.getAttribute("data-index-url");
23
+ const dialog = document.getElementById("command-search");
24
+ const command = dialog?.querySelector(".command");
25
+ const input = dialog?.querySelector("header input");
26
+ const menu = dialog?.querySelector('[role="menu"]');
27
+ const state = {
28
+ loadPromise: null,
29
+ index: null,
30
+ documents: [],
31
+ bySlug: new Map(),
32
+ };
33
+ const MAX_RESULTS = 20;
34
+ const SNIPPET_RADIUS = 80;
35
+
36
+ function escapeHtml(value) {
37
+ return String(value)
38
+ .replace(/&/g, "&amp;")
39
+ .replace(/</g, "&lt;")
40
+ .replace(/>/g, "&gt;")
41
+ .replace(/"/g, "&quot;");
42
+ }
43
+
44
+ function refreshCommand() {
45
+ if (!command) return;
46
+ if (typeof command.refresh !== "function") window.basecoat?.initAll?.();
47
+ if (typeof command.refresh === "function") {
48
+ command.refresh();
49
+ } else {
50
+ window.basecoat?.refresh?.(command);
51
+ }
52
+ }
53
+
54
+ function mergeSpans(spans) {
55
+ const sorted = spans
56
+ .filter(([start, end]) => end > start)
57
+ .sort((a, b) => a[0] - b[0]);
58
+ const merged = [];
59
+ sorted.forEach(([start, end]) => {
60
+ const last = merged.at(-1);
61
+ if (last && start <= last[1]) {
62
+ last[1] = Math.max(last[1], end);
63
+ } else {
64
+ merged.push([start, end]);
65
+ }
66
+ });
67
+ return merged;
68
+ }
69
+
70
+ function rangesFromMatch(match, field) {
71
+ if (!match?.matchData?.metadata) return [];
72
+ return mergeSpans(
73
+ Object.values(match.matchData.metadata)
74
+ .flatMap((metadata) => metadata?.[field]?.position || [])
75
+ .map(([start, length]) => [start, start + length]),
76
+ );
77
+ }
78
+
79
+ function queryTerms(query) {
80
+ return query
81
+ .toLowerCase()
82
+ .match(/[\p{L}\p{N}_]+/gu)
83
+ ?.filter(Boolean) || [];
84
+ }
85
+
86
+ function refineRanges(text, ranges, query) {
87
+ const terms = queryTerms(query);
88
+ if (!terms.length) return ranges;
89
+
90
+ const refined = ranges.flatMap(([start, end]) => {
91
+ const value = text.slice(start, end);
92
+ const lower = value.toLowerCase();
93
+ const termRanges = terms.flatMap((term) => {
94
+ const matches = [];
95
+ let index = lower.indexOf(term);
96
+ while (index > -1) {
97
+ matches.push([start + index, start + index + term.length]);
98
+ index = lower.indexOf(term, index + term.length);
99
+ }
100
+ return matches;
101
+ });
102
+ return termRanges.length ? termRanges : [[start, end]];
103
+ });
104
+
105
+ return mergeSpans(refined);
106
+ }
107
+
108
+ function highlightRanges(text, ranges) {
109
+ if (!ranges.length) return escapeHtml(text);
110
+ let cursor = 0;
111
+ let html = "";
112
+ ranges.forEach(([start, end]) => {
113
+ if (start > cursor) html += escapeHtml(text.slice(cursor, start));
114
+ html += `<mark>${escapeHtml(text.slice(start, end))}</mark>`;
115
+ cursor = end;
116
+ });
117
+ if (cursor < text.length) html += escapeHtml(text.slice(cursor));
118
+ return html;
119
+ }
120
+
121
+ function clampSnippetStart(text, start) {
122
+ if (start <= 0) return 0;
123
+ const space = text.lastIndexOf(" ", start);
124
+ return space > 0 ? space + 1 : start;
125
+ }
126
+
127
+ function clampSnippetEnd(text, end) {
128
+ if (end >= text.length) return text.length;
129
+ const space = text.indexOf(" ", end);
130
+ return space > -1 ? space : end;
131
+ }
132
+
133
+ function snippetFromBody(body, ranges) {
134
+ const text = String(body || "");
135
+ if (!text || !ranges.length) return "";
136
+
137
+ const first = ranges[0];
138
+ const start = clampSnippetStart(text, Math.max(0, first[0] - SNIPPET_RADIUS));
139
+ const end = clampSnippetEnd(text, Math.min(text.length, first[1] + SNIPPET_RADIUS));
140
+ const snippet = text.slice(start, end);
141
+ const offsetRanges = ranges
142
+ .map(([rangeStart, rangeEnd]) => [rangeStart - start, rangeEnd - start])
143
+ .filter(([rangeStart, rangeEnd]) => rangeEnd > 0 && rangeStart < snippet.length)
144
+ .map(([rangeStart, rangeEnd]) => [Math.max(0, rangeStart), Math.min(snippet.length, rangeEnd)]);
145
+
146
+ return `${start > 0 ? "..." : ""}${highlightRanges(snippet, mergeSpans(offsetRanges))}${end < text.length ? "..." : ""}`;
147
+ }
148
+
149
+ function resultView(result, query) {
150
+ const doc = result.doc || result;
151
+ if (!query.trim()) {
152
+ return {
153
+ path: doc.path,
154
+ title: escapeHtml(doc.title),
155
+ snippet: "",
156
+ };
157
+ }
158
+
159
+ const titleRanges = refineRanges(doc.title, rangesFromMatch(result.match, "title"), query);
160
+ const bodyRanges = refineRanges(doc.body, rangesFromMatch(result.match, "body"), query);
161
+ return {
162
+ path: doc.path,
163
+ title: highlightRanges(doc.title, titleRanges),
164
+ snippet: snippetFromBody(doc.body, bodyRanges),
165
+ };
166
+ }
167
+
168
+ function renderItems(items, label, query = "") {
169
+ if (!menu) return;
170
+ if (!items.length) {
171
+ menu.innerHTML = "";
172
+ refreshCommand();
173
+ return;
174
+ }
175
+
176
+ menu.innerHTML = `<div role="group" aria-labelledby="command-search-results-heading">
177
+ <span role="heading" id="command-search-results-heading">${escapeHtml(label)}</span>
178
+ ${items
179
+ .map((item, index) => {
180
+ const view = resultView(item, query);
181
+ return `<a id="command-search-result-${index}" role="menuitem" data-force data-rsd-search-result href="${escapeHtml(view.path)}">
182
+ <span data-rsd-search-title>${view.title}</span>
183
+ ${view.snippet ? `<span data-rsd-search-snippet>${view.snippet}</span>` : ""}
184
+ </a>`;
185
+ })
186
+ .join("")}
187
+ </div>`;
188
+ refreshCommand();
189
+ }
190
+
191
+ function renderLoading() {
192
+ if (!menu) return;
193
+ menu.innerHTML = `<div role="group" aria-labelledby="command-search-loading-heading">
194
+ <span role="heading" id="command-search-loading-heading">Search</span>
195
+ <div role="menuitem" aria-disabled="true" data-force><span>Loading...</span></div>
196
+ </div>`;
197
+ refreshCommand();
198
+ }
199
+
200
+ async function loadIndex() {
201
+ if (state.index) return state;
202
+ if (!indexUrl) throw new Error("Missing RSD search index URL.");
203
+ if (!state.loadPromise) {
204
+ renderLoading();
205
+ state.loadPromise = fetch(indexUrl)
206
+ .then((response) => response.json())
207
+ .then((data) => {
208
+ state.documents = data.documents || [];
209
+ state.bySlug = new Map(state.documents.map((doc) => [doc.slug, doc]));
210
+ state.index = lunr.Index.load(data.index);
211
+ return state;
212
+ });
213
+ }
214
+ return state.loadPromise;
215
+ }
216
+
217
+ function search(query) {
218
+ const normalized = query.trim();
219
+ if (!normalized) return state.documents;
220
+
221
+ const terms = normalized.match(/[\p{L}\p{N}_]+/gu) || [];
222
+ if (!terms.length) return [];
223
+
224
+ let matches = [];
225
+ try {
226
+ matches = state.index.search(terms.join(" "));
227
+ if (!matches.length) matches = state.index.search(terms.map((term) => `${term}*`).join(" "));
228
+ } catch {
229
+ matches = [];
230
+ }
231
+
232
+ return matches
233
+ .map((match) => {
234
+ const doc = state.bySlug.get(match.ref);
235
+ return doc ? { doc, match } : null;
236
+ })
237
+ .filter(Boolean);
238
+ }
239
+
240
+ async function refreshResults() {
241
+ await loadIndex();
242
+ const query = input?.value || "";
243
+ const results = search(query).slice(0, MAX_RESULTS);
244
+ renderItems(results, query.trim() ? "Results" : "Docs", query);
245
+ }
246
+
247
+ window.rsdOpenCommandSearch = async () => {
248
+ if (!dialog || !input) return;
249
+ if (!dialog.open) dialog.showModal();
250
+ input.focus();
251
+ await refreshResults();
252
+ };
253
+
254
+ input?.addEventListener("input", () => {
255
+ refreshResults();
256
+ });
257
+
258
+ document.addEventListener("keydown", (event) => {
259
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
260
+ event.preventDefault();
261
+ if (!dialog) return;
262
+ if (dialog.open) {
263
+ dialog.close();
264
+ } else {
265
+ window.rsdOpenCommandSearch?.();
266
+ }
267
+ }
268
+ });
269
+ </script>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import { escapeHtml } from "../lib/html.js";
3
+
4
+ const { site } = Astro.props;
5
+ const logo = site.logo?.svg
6
+ ? site.logo.svg
7
+ : site.logo?.url
8
+ ? `<img src="${escapeHtml(site.logo.url)}" alt="${escapeHtml(site.title)} logo" class="size-6" />`
9
+ : "";
10
+ ---
11
+
12
+ <a href="/" class="btn-ghost h-12 w-full justify-start p-2">
13
+ {
14
+ logo && (
15
+ <div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
16
+ <Fragment set:html={logo} />
17
+ </div>
18
+ )
19
+ }
20
+ <div class="grid flex-1 text-left text-sm leading-tight">
21
+ <span class="truncate font-semibold">{site.title}</span>
22
+ {site.subtitle && <span class="truncate text-xs text-muted-foreground">{site.subtitle}</span>}
23
+ </div>
24
+ </a>
@@ -0,0 +1,287 @@
1
+ ---
2
+ import config from "virtual:reallysimpledocs/config";
3
+ import ArrowLeft from "lucide-static/icons/arrow-left.svg?raw";
4
+ import ArrowRight from "lucide-static/icons/arrow-right.svg?raw";
5
+ import Check from "lucide-static/icons/check.svg?raw";
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
+ import PanelLeft from "lucide-static/icons/panel-left.svg?raw";
10
+ 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";
13
+ import ClaudeLogo from "../../icons/logos/claude.svg?raw";
14
+ import CursorLogo from "../../icons/logos/cursor.svg?raw";
15
+ import MarkdownLogo from "../../icons/logos/markdown.svg?raw";
16
+ import OpenAILogo from "../../icons/logos/openai.svg?raw";
17
+ import BaseLayout from "./BaseLayout.astro";
18
+ import CommandDialog from "./CommandDialog.astro";
19
+ import DropdownMenu from "./DropdownMenu.astro";
20
+ import Sidebar from "./Sidebar.astro";
21
+ import { buildMenus, getNavigation } from "../lib/docs.js";
22
+ import { addSvgClass } from "../lib/html.js";
23
+ import { routeFilePath } from "../lib/paths.js";
24
+ import { getSite } from "../lib/site.js";
25
+
26
+ const { page, headings } = Astro.props;
27
+ const site = getSite(config);
28
+ const navigation = getNavigation(config, page);
29
+ const { sidebar } = buildMenus(config, page.slug);
30
+ const searchIndexUrl = routeFilePath(config.routeBase, "search-index.json");
31
+ const panelLeftIcon = addSvgClass(PanelLeft, "size-4");
32
+ const searchIcon = addSvgClass(Search, "size-4");
33
+ const siteUrl = Astro.site?.toString().replace(/\/$/, "") || site.url || "";
34
+ const pageUrl = `${siteUrl}${page.path}`;
35
+ const llmPrompt = `Read from ${pageUrl} so I can ask questions about it.`;
36
+ const actionItems = [
37
+ {
38
+ icon: MarkdownLogo,
39
+ label: "View as Markdown",
40
+ url: page.markdownPath,
41
+ target: "_blank",
42
+ rel: "noopener",
43
+ },
44
+ {
45
+ icon: OpenAILogo,
46
+ label: "Open in ChatGPT",
47
+ url: `https://chatgpt.com/?q=${encodeURIComponent(llmPrompt)}`,
48
+ target: "_blank",
49
+ rel: "noopener",
50
+ },
51
+ {
52
+ icon: ClaudeLogo,
53
+ label: "Open in Claude",
54
+ url: `https://claude.ai/new?q=${encodeURIComponent(llmPrompt)}`,
55
+ target: "_blank",
56
+ rel: "noopener",
57
+ },
58
+ {
59
+ icon: CursorLogo,
60
+ label: "Open in Cursor",
61
+ url: `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(llmPrompt)}`,
62
+ target: "_blank",
63
+ rel: "noopener",
64
+ },
65
+ ];
66
+ ---
67
+
68
+ <BaseLayout title={page.title} description={site.description} pagePath={page.path}>
69
+ <CommandDialog indexUrl={searchIndexUrl} />
70
+ <Sidebar menu={sidebar} />
71
+
72
+ <main id="content">
73
+ <header class="bg-background sticky inset-x-0 top-0 isolate z-10 flex shrink-0 items-center gap-2 border-b">
74
+ <div class="flex h-14 w-full items-center justify-between gap-2 px-4">
75
+ <button
76
+ type="button"
77
+ onclick="document.getElementById('sidebar')?.toggle()"
78
+ aria-label="Toggle sidebar"
79
+ data-tooltip="Toggle sidebar"
80
+ data-side="bottom"
81
+ data-align="start"
82
+ class="btn-sm-icon-ghost"
83
+ >
84
+ <Fragment set:html={panelLeftIcon} />
85
+ </button>
86
+
87
+ <button
88
+ 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"
90
+ onclick="window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal()"
91
+ >
92
+ <Fragment set:html={searchIcon} />
93
+ Search the docs...
94
+ <kbd class="kbd ml-auto -mr-1.5">⌘K</kbd>
95
+ </button>
96
+
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>
127
+
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>
155
+ </div>
156
+ </header>
157
+
158
+ <div class="flex gap-10 p-4 md:p-6 xl:p-12">
159
+ <article class="min-w-0 flex-1 wrap-break-word">
160
+ <div class="mx-auto w-full max-w-2xl flex-1 space-y-10">
161
+ <header class="space-y-2">
162
+ <div class="flex items-start gap-x-2">
163
+ <h1 class="mr-auto text-3xl font-semibold tracking-tight sm:text-4xl">{page.title}</h1>
164
+ <div class="mt-1 flex items-center gap-x-2">
165
+ <div role="group" class="button-group">
166
+ <button
167
+ type="button"
168
+ class="btn-sm-secondary"
169
+ data-md-url={page.markdownPath}
170
+ onclick="copyPageMarkdown(this)"
171
+ >
172
+ <span data-copy-icon><Fragment set:html={Copy} /></span>
173
+ <span data-check-icon class="hidden"><Fragment set:html={Check} /></span>
174
+ <span>Copy page</span>
175
+ </button>
176
+ <hr role="separator" />
177
+ <DropdownMenu items={actionItems} />
178
+ </div>
179
+ <div role="group" class="button-group">
180
+ {
181
+ navigation.prev && (
182
+ <a href={navigation.prev.path} class="btn-sm-icon-secondary">
183
+ <span class="sr-only">{navigation.prev.title}</span>
184
+ <Fragment set:html={ArrowLeft} />
185
+ </a>
186
+ )
187
+ }
188
+ {navigation.prev && navigation.next && <hr role="separator" />}
189
+ {
190
+ navigation.next && (
191
+ <a href={navigation.next.path} class="btn-sm-icon-secondary">
192
+ <span class="sr-only">{navigation.next.title}</span>
193
+ <Fragment set:html={ArrowRight} />
194
+ </a>
195
+ )
196
+ }
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </header>
201
+ <div class="prose">
202
+ <slot />
203
+ </div>
204
+ <footer class="mt-12 flex items-center">
205
+ {
206
+ navigation.prev && (
207
+ <a href={navigation.prev.path} class="btn-sm-secondary mr-auto">
208
+ <Fragment set:html={ArrowLeft} />
209
+ {navigation.prev.title}
210
+ </a>
211
+ )
212
+ }
213
+ {
214
+ navigation.next && (
215
+ <a href={navigation.next.path} class="btn-sm-secondary ml-auto">
216
+ {navigation.next.title}
217
+ <Fragment set:html={ArrowRight} />
218
+ </a>
219
+ )
220
+ }
221
+ </footer>
222
+ </div>
223
+ </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-primary [&_ol]:space-y-2 [&_ol_ol]:mt-2 [&_ol_ol]:pl-3">
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}`}>{heading.text}</a>
234
+ </li>
235
+ ))
236
+ }
237
+ </ol>
238
+ </div>
239
+ </aside>
240
+ </div>
241
+ </main>
242
+
243
+ <script is:inline>
244
+ async function copyPageMarkdown(button) {
245
+ const markdownUrl = button?.dataset?.mdUrl;
246
+ if (!markdownUrl) return;
247
+ const copyIcon = button.querySelector("[data-copy-icon]");
248
+ const checkIcon = button.querySelector("[data-check-icon]");
249
+
250
+ try {
251
+ const response = await fetch(markdownUrl, { credentials: "same-origin", cache: "no-store" });
252
+ if (!response.ok) throw new Error("Failed to fetch markdown");
253
+ const markdown = await response.text();
254
+ await writeToClipboard(markdown);
255
+ if (copyIcon && checkIcon) {
256
+ copyIcon.classList.add("hidden");
257
+ checkIcon.classList.remove("hidden");
258
+ window.setTimeout(() => {
259
+ copyIcon.classList.remove("hidden");
260
+ checkIcon.classList.add("hidden");
261
+ }, 1200);
262
+ }
263
+ } catch (_) {
264
+ window.open(new URL(markdownUrl, window.location.href).toString(), "_blank", "noopener");
265
+ }
266
+ }
267
+
268
+ async function writeToClipboard(text) {
269
+ if (navigator.clipboard?.writeText && window.isSecureContext) {
270
+ await navigator.clipboard.writeText(text);
271
+ return;
272
+ }
273
+
274
+ const textarea = document.createElement("textarea");
275
+ textarea.value = text;
276
+ textarea.setAttribute("readonly", "");
277
+ textarea.style.position = "fixed";
278
+ textarea.style.top = "-9999px";
279
+ textarea.style.left = "-9999px";
280
+ document.body.appendChild(textarea);
281
+ textarea.select();
282
+ const ok = document.execCommand("copy");
283
+ document.body.removeChild(textarea);
284
+ if (!ok) throw new Error("Copy failed");
285
+ }
286
+ </script>
287
+ </BaseLayout>
@@ -0,0 +1,22 @@
1
+ ---
2
+ import ChevronDown from "lucide-static/icons/chevron-down.svg?raw";
3
+ import { renderDropdownMenu } from "../lib/macros.js";
4
+
5
+ const { items } = Astro.props;
6
+ ---
7
+
8
+ <Fragment
9
+ set:html={renderDropdownMenu({
10
+ id: "page-actions",
11
+ trigger: ChevronDown,
12
+ items: items.map((item) => ({
13
+ ...item,
14
+ attrs: {
15
+ target: item.target,
16
+ rel: item.rel,
17
+ },
18
+ })),
19
+ triggerAttrs: { class: "btn-sm-icon-secondary" },
20
+ popoverAttrs: { "data-align": "end" },
21
+ })}
22
+ />
@@ -0,0 +1,20 @@
1
+ ---
2
+ import config from "virtual:reallysimpledocs/config";
3
+ import SidebarHeader from "virtual:reallysimpledocs/components/SidebarHeader";
4
+ import { renderSidebarContent } from "../lib/macros.js";
5
+ import { getSite } from "../lib/site.js";
6
+
7
+ const { menu } = Astro.props;
8
+ const site = getSite(config);
9
+ ---
10
+
11
+ <aside id="sidebar" class="sidebar" data-side="left" aria-hidden="false">
12
+ <nav aria-label="Sidebar navigation">
13
+ <header>
14
+ <SidebarHeader site={site} />
15
+ </header>
16
+ <section class="scrollbar">
17
+ <Fragment set:html={renderSidebarContent(menu, "sidebar-content")} />
18
+ </section>
19
+ </nav>
20
+ </aside>