reallysimpledocs 1.0.0-beta.3 → 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 (43) hide show
  1. package/README.md +33 -31
  2. package/package.json +10 -5
  3. package/src/astro/index.js +46 -43
  4. package/src/astro/preview-source.js +90 -0
  5. package/src/components/index.js +1 -0
  6. package/src/css/custom.css +259 -29
  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 +29 -14
  17. package/src/runtime/components/DefaultContentHeader.astro +2 -0
  18. package/src/runtime/components/DefaultHead.astro +120 -0
  19. package/src/runtime/components/DefaultHeadContent.astro +2 -0
  20. package/src/runtime/components/DefaultSidebarFooter.astro +2 -0
  21. package/src/runtime/components/DefaultSidebarHeader.astro +1 -1
  22. package/src/runtime/components/DocsLayout.astro +90 -122
  23. package/src/runtime/components/DropdownMenu.astro +1 -1
  24. package/src/runtime/components/Sidebar.astro +4 -0
  25. package/src/runtime/components/ThemeToggle.astro +21 -0
  26. package/src/runtime/components/mdx/Callout.astro +2 -9
  27. package/src/runtime/components/mdx/Card.astro +3 -8
  28. package/src/runtime/components/mdx/CardGrid.astro +1 -1
  29. package/src/runtime/components/mdx/Code.astro +2 -16
  30. package/src/runtime/components/mdx/CodeGroup.astro +7 -11
  31. package/src/runtime/components/mdx/Preview.astro +7 -12
  32. package/src/runtime/components/mdx/Steps.astro +1 -5
  33. package/src/runtime/components/mdx/Tab.astro +6 -0
  34. package/src/runtime/components/mdx/Tabs.astro +7 -7
  35. package/src/runtime/lib/docs.js +122 -16
  36. package/src/runtime/lib/highlight.js +1 -14
  37. package/src/runtime/lib/macros.js +11 -2
  38. package/src/runtime/lib/markdown-export.js +163 -0
  39. package/src/runtime/lib/site.js +0 -1
  40. package/src/runtime/pages/search-index.ts +10 -1
  41. package/src/css/overrides.css +0 -1
  42. package/src/css/sources.css +0 -1
  43. package/src/runtime/scripts/copy-code.js +0 -109
@@ -0,0 +1,120 @@
1
+ ---
2
+ import Copy from "lucide-static/icons/copy.svg?raw";
3
+ import Check from "lucide-static/icons/check.svg?raw";
4
+ import basecoatJsUrl from "basecoat-css/all.min?url";
5
+ ---
6
+
7
+ <script is:inline>
8
+ (() => {
9
+ try {
10
+ const stored = localStorage.getItem("themeMode");
11
+ if (stored ? stored === "dark" : matchMedia("(prefers-color-scheme: dark)").matches) {
12
+ document.documentElement.classList.add("dark");
13
+ }
14
+ } catch (_) {}
15
+ })();
16
+ </script>
17
+ <script src={basecoatJsUrl}></script>
18
+ <script is:inline define:vars={{ copyIcon: Copy, checkIcon: Check }}>
19
+ const copyText = async (text) => {
20
+ if (window.isSecureContext && navigator.clipboard?.writeText) {
21
+ return navigator.clipboard.writeText(text);
22
+ }
23
+
24
+ const textarea = document.createElement("textarea");
25
+ textarea.value = text;
26
+ textarea.setAttribute("readonly", "");
27
+ textarea.style.position = "fixed";
28
+ textarea.style.top = "-9999px";
29
+ textarea.style.left = "-9999px";
30
+ document.body.appendChild(textarea);
31
+ textarea.select();
32
+ const ok = document.execCommand("copy");
33
+ textarea.remove();
34
+ if (!ok) throw new Error("Copy failed");
35
+ };
36
+
37
+ const activePanelCode = (root) => root.querySelector(':scope > [role="tabpanel"]:not([hidden]) code');
38
+
39
+ const initCopyCode = (codeBlock) => {
40
+ if (codeBlock.dataset.copyCodeInitialized) return;
41
+ const codeEl = codeBlock.querySelector(":scope > code");
42
+ if (!codeEl) return;
43
+
44
+ let target = null;
45
+ let frame = codeBlock.closest(".code-group");
46
+ let getCode = frame ? () => activePanelCode(frame) || codeEl : () => codeEl;
47
+ if (frame) target = frame.querySelector(":scope > header");
48
+
49
+ const preview = codeBlock.closest(".preview");
50
+ const previewCodePanel = preview?.querySelector(":scope > [data-code-panel]");
51
+ if (!frame && previewCodePanel?.contains(codeBlock)) {
52
+ frame = preview;
53
+ target = frame.querySelector(":scope > header");
54
+ getCode = () => codeEl;
55
+ }
56
+
57
+ if (!frame) {
58
+ frame = codeBlock.parentElement?.classList.contains("code-block") ? codeBlock.parentElement : null;
59
+ if (!frame) {
60
+ frame = document.createElement("div");
61
+ frame.className = "code-block";
62
+ codeBlock.parentNode.insertBefore(frame, codeBlock);
63
+ frame.appendChild(codeBlock);
64
+ }
65
+ }
66
+
67
+ target ||= frame.querySelector(":scope > header") || frame;
68
+ if (target.querySelector(":scope > button[aria-label='Copy code']")) {
69
+ codeBlock.dataset.copyCodeInitialized = "true";
70
+ return;
71
+ }
72
+
73
+ const button = document.createElement("button");
74
+ button.type = "button";
75
+ button.className = "btn";
76
+ button.dataset.variant = "ghost";
77
+ button.dataset.size = "icon-xs";
78
+ button.setAttribute("aria-label", "Copy code");
79
+ button.innerHTML = copyIcon;
80
+
81
+ button.addEventListener("click", async () => {
82
+ const text = (getCode()?.textContent || "").replace(/\n$/, "");
83
+ if (!text) return;
84
+
85
+ try {
86
+ await copyText(text);
87
+ button.innerHTML = checkIcon;
88
+ } catch (error) {
89
+ console.error("[copy] Failed to copy:", error);
90
+ button.textContent = "Failed";
91
+ }
92
+ window.setTimeout(() => {
93
+ button.innerHTML = copyIcon;
94
+ }, 1200);
95
+ });
96
+
97
+ target.appendChild(button);
98
+ codeBlock.dataset.copyCodeInitialized = "true";
99
+ };
100
+
101
+ const initCopyButtons = (root = document) => {
102
+ root.querySelectorAll("pre:has(> code):not([data-copy-code-initialized])").forEach(initCopyCode);
103
+ };
104
+
105
+ if (document.readyState === "loading") {
106
+ document.addEventListener("DOMContentLoaded", () => initCopyButtons(), { once: true });
107
+ } else {
108
+ initCopyButtons();
109
+ }
110
+
111
+ new MutationObserver((mutations) => {
112
+ for (const mutation of mutations) {
113
+ mutation.addedNodes.forEach((node) => {
114
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
115
+ if (node.matches?.("pre:has(> code)")) initCopyCode(node);
116
+ initCopyButtons(node);
117
+ });
118
+ }
119
+ }).observe(document.documentElement, { childList: true, subtree: true });
120
+ </script>
@@ -0,0 +1,2 @@
1
+ ---
2
+ ---
@@ -9,7 +9,7 @@ const logo = site.logo?.svg
9
9
  : "";
10
10
  ---
11
11
 
12
- <a href="/" class="btn-ghost h-12 w-full justify-start p-2">
12
+ <a href="/" class="btn h-12 w-full justify-start p-2" data-variant="ghost" data-size="lg">
13
13
  {
14
14
  logo && (
15
15
  <div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
@@ -5,29 +5,26 @@ 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
7
  import Menu from "lucide-static/icons/menu.svg?raw";
8
- import Moon from "lucide-static/icons/moon.svg?raw";
9
8
  import PanelLeft from "lucide-static/icons/panel-left.svg?raw";
10
9
  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
10
  import ClaudeLogo from "../../icons/logos/claude.svg?raw";
14
11
  import CursorLogo from "../../icons/logos/cursor.svg?raw";
15
12
  import MarkdownLogo from "../../icons/logos/markdown.svg?raw";
16
13
  import OpenAILogo from "../../icons/logos/openai.svg?raw";
17
14
  import BaseLayout from "./BaseLayout.astro";
18
15
  import CommandDialog from "./CommandDialog.astro";
16
+ import ContentHeader from "virtual:reallysimpledocs/components/ContentHeader";
19
17
  import DropdownMenu from "./DropdownMenu.astro";
20
18
  import Sidebar from "./Sidebar.astro";
19
+ import ThemeToggle from "./ThemeToggle.astro";
21
20
  import { buildMenus, getNavigation } from "../lib/docs.js";
22
21
  import { addSvgClass } from "../lib/html.js";
23
- import { routeFilePath } from "../lib/paths.js";
24
22
  import { getSite } from "../lib/site.js";
25
23
 
26
24
  const { page, headings } = Astro.props;
27
25
  const site = getSite(config);
28
26
  const navigation = getNavigation(config, page);
29
27
  const { sidebar } = buildMenus(config, page.slug);
30
- const searchIndexUrl = routeFilePath(config.routeBase, "search-index.json");
31
28
  const panelLeftIcon = addSvgClass(PanelLeft, "size-4");
32
29
  const searchIcon = addSvgClass(Search, "size-4");
33
30
  const siteUrl = Astro.site?.toString().replace(/\/$/, "") || site.url || "";
@@ -66,11 +63,13 @@ const actionItems = [
66
63
  ---
67
64
 
68
65
  <BaseLayout title={page.title} description={site.description} pagePath={page.path}>
69
- <CommandDialog indexUrl={searchIndexUrl} />
66
+ <CommandDialog />
70
67
  <Sidebar menu={sidebar} />
71
68
 
72
69
  <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">
70
+ <header
71
+ class="sticky inset-x-0 top-0 isolate z-30 flex shrink-0 items-center gap-2 border-b bg-background"
72
+ >
74
73
  <div class="flex h-14 w-full items-center justify-between gap-2 px-4">
75
74
  <button
76
75
  type="button"
@@ -79,78 +78,42 @@ const actionItems = [
79
78
  data-tooltip="Toggle sidebar"
80
79
  data-side="bottom"
81
80
  data-align="start"
82
- class="btn-sm-icon-ghost"
81
+ class="btn"
82
+ data-variant="ghost"
83
+ data-size="icon-sm"
83
84
  >
84
85
  <Fragment set:html={panelLeftIcon} />
85
86
  </button>
86
87
 
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"
88
+ <div
89
+ class="input-group h-8 w-full min-w-0 max-w-72 cursor-text sm:ml-auto"
90
+ role="button"
91
+ tabindex="0"
92
+ aria-label="Search docs"
90
93
  onclick="window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal()"
94
+ onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal(); }"
91
95
  >
92
- <Fragment set:html={searchIcon} />
93
- Search the docs...
94
- <kbd class="kbd ml-auto -mr-1.5">⌘K</kbd>
95
- </button>
96
+ <input type="text" placeholder="Search..." readonly tabindex="-1" aria-hidden="true" />
97
+ <span data-align="start" aria-hidden="true"><Fragment set:html={searchIcon} /></span>
98
+ <span data-align="end" aria-hidden="true"><kbd class="kbd">⌘K</kbd></span>
99
+ </div>
96
100
 
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>
101
+ <ContentHeader site={site} page={page} />
102
+ <ThemeToggle />
127
103
 
128
104
  <div class="sm:hidden">
129
105
  <button
130
106
  type="button"
131
- class="btn-sm-icon-ghost peer group"
132
- aria-label="Toggle menu"
107
+ class="btn peer group"
108
+ data-variant="ghost"
109
+ data-size="icon-sm"
110
+ aria-label="Toggle sidebar"
133
111
  aria-expanded="false"
134
- aria-controls="mobile-menu"
135
- onclick="this.setAttribute('aria-expanded', this.getAttribute('aria-expanded') === 'true' ? 'false' : 'true')"
112
+ aria-controls="sidebar"
113
+ onclick="document.getElementById('sidebar')?.toggle()"
136
114
  >
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>
115
+ <Fragment set:html={Menu} />
139
116
  </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
117
  </div>
155
118
  </div>
156
119
  </header>
@@ -159,19 +122,20 @@ const actionItems = [
159
122
  <article class="min-w-0 flex-1 wrap-break-word">
160
123
  <div class="mx-auto w-full max-w-2xl flex-1 space-y-10">
161
124
  <header class="space-y-2">
162
- <div class="flex items-start gap-x-2">
125
+ <div class="flex flex-col-reverse items-start gap-3 sm:flex-row sm:gap-x-2">
163
126
  <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">
127
+ <div class="flex w-full items-center justify-end gap-x-2 sm:mt-1 sm:w-auto">
165
128
  <div role="group" class="button-group">
166
129
  <button
167
130
  type="button"
168
- class="btn-sm-secondary"
131
+ class="btn"
132
+ data-variant="secondary"
133
+ data-size="sm"
169
134
  data-md-url={page.markdownPath}
170
135
  onclick="copyPageMarkdown(this)"
171
136
  >
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>
137
+ <Fragment set:html={Copy} />
138
+ Copy page
175
139
  </button>
176
140
  <hr role="separator" />
177
141
  <DropdownMenu items={actionItems} />
@@ -179,7 +143,7 @@ const actionItems = [
179
143
  <div role="group" class="button-group">
180
144
  {
181
145
  navigation.prev && (
182
- <a href={navigation.prev.path} class="btn-sm-icon-secondary">
146
+ <a href={navigation.prev.path} class="btn" data-variant="secondary" data-size="icon-sm">
183
147
  <span class="sr-only">{navigation.prev.title}</span>
184
148
  <Fragment set:html={ArrowLeft} />
185
149
  </a>
@@ -188,7 +152,7 @@ const actionItems = [
188
152
  {navigation.prev && navigation.next && <hr role="separator" />}
189
153
  {
190
154
  navigation.next && (
191
- <a href={navigation.next.path} class="btn-sm-icon-secondary">
155
+ <a href={navigation.next.path} class="btn" data-variant="secondary" data-size="icon-sm">
192
156
  <span class="sr-only">{navigation.next.title}</span>
193
157
  <Fragment set:html={ArrowRight} />
194
158
  </a>
@@ -204,7 +168,7 @@ const actionItems = [
204
168
  <footer class="mt-12 flex items-center">
205
169
  {
206
170
  navigation.prev && (
207
- <a href={navigation.prev.path} class="btn-sm-secondary mr-auto">
171
+ <a href={navigation.prev.path} class="btn mr-auto" data-variant="secondary" data-size="sm">
208
172
  <Fragment set:html={ArrowLeft} />
209
173
  {navigation.prev.title}
210
174
  </a>
@@ -212,7 +176,7 @@ const actionItems = [
212
176
  }
213
177
  {
214
178
  navigation.next && (
215
- <a href={navigation.next.path} class="btn-sm-secondary ml-auto">
179
+ <a href={navigation.next.path} class="btn ml-auto" data-variant="secondary" data-size="sm">
216
180
  {navigation.next.title}
217
181
  <Fragment set:html={ArrowRight} />
218
182
  </a>
@@ -222,7 +186,7 @@ const actionItems = [
222
186
  </div>
223
187
  </article>
224
188
  <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>
189
+ <div class="docs-toc sticky top-20 space-y-2 text-sm [&_a]:text-muted-foreground [&_a]:hover:text-foreground [&_a[aria-current='location']]:text-foreground [&_ol]:space-y-2 [&_ol_ol]:mt-2 [&_ol_ol]:pl-3" data-docs-toc>
226
190
  <h2 class="flex items-center gap-1 font-medium">
227
191
  On this page
228
192
  </h2>
@@ -240,30 +204,29 @@ const actionItems = [
240
204
  </div>
241
205
  </main>
242
206
 
243
- <script is:inline>
244
- async function copyPageMarkdown(button) {
207
+ <script is:inline define:vars={{ checkIcon: Check }}>
208
+ window.copyPageMarkdown = async function copyPageMarkdown(button) {
245
209
  const markdownUrl = button?.dataset?.mdUrl;
246
210
  if (!markdownUrl) return;
247
- const copyIcon = button.querySelector("[data-copy-icon]");
248
- const checkIcon = button.querySelector("[data-check-icon]");
211
+ const icon = button.querySelector("svg");
212
+ const originalIcon = icon?.outerHTML || "";
249
213
 
250
214
  try {
251
215
  const response = await fetch(markdownUrl, { credentials: "same-origin", cache: "no-store" });
252
216
  if (!response.ok) throw new Error("Failed to fetch markdown");
253
217
  const markdown = await response.text();
254
218
  await writeToClipboard(markdown);
255
- if (copyIcon && checkIcon) {
256
- copyIcon.classList.add("hidden");
257
- checkIcon.classList.remove("hidden");
219
+ if (icon && checkIcon) {
220
+ icon.outerHTML = checkIcon;
258
221
  window.setTimeout(() => {
259
- copyIcon.classList.remove("hidden");
260
- checkIcon.classList.add("hidden");
222
+ const currentIcon = button.querySelector("svg");
223
+ if (currentIcon && originalIcon) currentIcon.outerHTML = originalIcon;
261
224
  }, 1200);
262
225
  }
263
226
  } catch (_) {
264
227
  window.open(new URL(markdownUrl, window.location.href).toString(), "_blank", "noopener");
265
228
  }
266
- }
229
+ };
267
230
 
268
231
  async function writeToClipboard(text) {
269
232
  if (navigator.clipboard?.writeText && window.isSecureContext) {
@@ -286,55 +249,60 @@ const actionItems = [
286
249
 
287
250
  function initDocsToc() {
288
251
  const toc = document.querySelector("[data-docs-toc]");
289
- if (!toc) return;
252
+ const links = Array.from(toc?.querySelectorAll("[data-toc-link]") || []);
253
+ if (!links.length) return;
290
254
 
291
- const links = Array.from(toc.querySelectorAll("[data-toc-link]"));
292
- const linkById = new Map();
293
- for (const link of links) {
294
- const id = decodeURIComponent(link.hash.slice(1));
295
- if (id) linkById.set(id, link);
296
- }
297
-
298
- const headings = Array.from(linkById.keys())
299
- .map((id) => document.getElementById(id))
255
+ const entries = links
256
+ .map((link) => {
257
+ const id = decodeURIComponent(link.hash.slice(1));
258
+ const heading = id ? document.getElementById(id) : null;
259
+ return heading ? { id, heading, link } : null;
260
+ })
300
261
  .filter(Boolean);
301
- if (!headings.length) return;
262
+ if (!entries.length) return;
302
263
 
303
- const setActive = (id) => {
304
- for (const link of links) {
305
- const active = decodeURIComponent(link.hash.slice(1)) === id;
306
- link.dataset.active = active ? "true" : "false";
307
- if (active) link.setAttribute("aria-current", "location");
308
- else link.removeAttribute("aria-current");
264
+ const activate = (id) => {
265
+ for (const entry of entries) {
266
+ const active = entry.id === id;
267
+ entry.link.toggleAttribute("data-active", active);
268
+ if (active) entry.link.setAttribute("aria-current", "location");
269
+ else entry.link.removeAttribute("aria-current");
309
270
  }
310
271
  };
311
272
 
312
- const currentHeading = () => {
313
- const offset = 160;
314
- let current = headings[0];
315
- for (const heading of headings) {
316
- if (heading.getBoundingClientRect().top <= offset) current = heading;
273
+ const activeFromScroll = () => {
274
+ const marker = Math.min(window.innerHeight * 0.35, 220);
275
+ let current = entries[0];
276
+
277
+ for (const entry of entries) {
278
+ if (entry.heading.getBoundingClientRect().top <= marker) current = entry;
317
279
  else break;
318
280
  }
319
- return current;
320
- };
321
281
 
322
- const updateActive = () => setActive(currentHeading().id);
323
- updateActive();
282
+ activate(current.id);
283
+ };
324
284
 
325
- let queued = false;
326
- const queueUpdate = () => {
327
- if (queued) return;
328
- queued = true;
329
- window.requestAnimationFrame(() => {
330
- queued = false;
331
- updateActive();
285
+ let frame = 0;
286
+ const queueActiveUpdate = () => {
287
+ if (frame) return;
288
+ frame = window.requestAnimationFrame(() => {
289
+ frame = 0;
290
+ activeFromScroll();
332
291
  });
333
292
  };
334
293
 
335
- window.addEventListener("scroll", queueUpdate, { passive: true });
336
- window.addEventListener("resize", queueUpdate);
337
- window.addEventListener("hashchange", () => window.setTimeout(updateActive, 0));
294
+ if ("IntersectionObserver" in window) {
295
+ const observer = new IntersectionObserver(queueActiveUpdate, {
296
+ rootMargin: "-20% 0px -65% 0px",
297
+ threshold: [0, 1],
298
+ });
299
+ for (const entry of entries) observer.observe(entry.heading);
300
+ }
301
+
302
+ activeFromScroll();
303
+ window.addEventListener("scroll", queueActiveUpdate, { passive: true });
304
+ window.addEventListener("resize", queueActiveUpdate);
305
+ window.addEventListener("hashchange", () => window.setTimeout(activeFromScroll, 0));
338
306
  }
339
307
 
340
308
  initDocsToc();
@@ -16,7 +16,7 @@ const { items } = Astro.props;
16
16
  rel: item.rel,
17
17
  },
18
18
  })),
19
- triggerAttrs: { class: "btn-sm-icon-secondary" },
19
+ triggerAttrs: { class: "btn", "data-variant": "secondary", "data-size": "icon-sm" },
20
20
  popoverAttrs: { "data-align": "end" },
21
21
  })}
22
22
  />
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import config from "virtual:reallysimpledocs/config";
3
3
  import SidebarHeader from "virtual:reallysimpledocs/components/SidebarHeader";
4
+ import SidebarFooter from "virtual:reallysimpledocs/components/SidebarFooter";
4
5
  import { renderSidebarContent } from "../lib/macros.js";
5
6
  import { getSite } from "../lib/site.js";
6
7
 
@@ -16,5 +17,8 @@ const site = getSite(config);
16
17
  <section class="scrollbar">
17
18
  <Fragment set:html={renderSidebarContent(menu, "sidebar-content")} />
18
19
  </section>
20
+ <footer>
21
+ <SidebarFooter site={site} />
22
+ </footer>
19
23
  </nav>
20
24
  </aside>
@@ -0,0 +1,21 @@
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="window.basecoat?.theme.toggle()"
14
+ class="btn"
15
+ data-variant="ghost"
16
+ data-size="icon-sm"
17
+ >
18
+ <span class="hidden dark:block"><Fragment set:html={Sun} /></span>
19
+ <span class="block dark:hidden"><Fragment set:html={Moon} /></span>
20
+ </button>
21
+ </nav>
@@ -4,8 +4,7 @@ import { addSvgClass } from "../../lib/html.js";
4
4
 
5
5
  const { type, title = "Note", icon: iconName, class: className = "" } = Astro.props;
6
6
  const normalizedType = String(type || "").toLowerCase();
7
- const calloutType = normalizedType === "warning" || normalizedType === "danger" ? normalizedType : "";
8
- const danger = calloutType === "danger";
7
+ const calloutType = normalizedType === "warning" || normalizedType === "destructive" ? normalizedType : "";
9
8
 
10
9
  function resolveNamedIcon(name) {
11
10
  if (!name || typeof name !== "string") return "";
@@ -24,15 +23,9 @@ function resolveNamedIcon(name) {
24
23
  const iconSource = typeof iconName === "string" ? resolveNamedIcon(iconName) : "";
25
24
  const icon = addSvgClass(iconSource, "size-4");
26
25
  const heading = title;
27
- const variantClass =
28
- calloutType === "warning"
29
- ? "[&>svg]:text-amber-700 [&>h3]:text-amber-700 [&>section]:text-amber-700 dark:[&>svg]:text-amber-300 dark:[&>h3]:text-amber-300 dark:[&>section]:text-amber-300"
30
- : danger
31
- ? "[&>svg]:text-destructive [&>h3]:text-destructive [&>section]:text-destructive"
32
- : "";
33
26
  ---
34
27
 
35
- <div class:list={[danger ? "alert-destructive" : "alert", "my-6", variantClass, className]}>
28
+ <div class:list={["alert", className]} data-variant={calloutType || undefined}>
36
29
  {icon && <Fragment set:html={icon} />}
37
30
  <h3>{heading}</h3>
38
31
  <section>
@@ -1,14 +1,9 @@
1
1
  ---
2
2
  const { title, description, href, class: className = "" } = Astro.props;
3
3
  const Tag = href ? "a" : "article";
4
- const classes = [
5
- "my-6 block w-full rounded-lg border bg-card p-3 text-card-foreground no-underline",
6
- href && "transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
7
- className,
8
- ];
9
4
  ---
10
5
 
11
- <Tag href={href} class:list={classes}>
12
- {title && <h3 class="font-medium">{title}</h3>}
13
- {description && <p class="mt-1 text-sm text-muted-foreground">{description}</p>}
6
+ <Tag href={href} class:list={["docs-card", href && "docs-card-link", className]}>
7
+ {title && <h3>{title}</h3>}
8
+ {description && <p>{description}</p>}
14
9
  </Tag>
@@ -2,6 +2,6 @@
2
2
  const { class: className = "" } = Astro.props;
3
3
  ---
4
4
 
5
- <div class:list={["my-6 grid w-full gap-4 sm:grid-cols-2 [&>*]:my-0", className]}>
5
+ <div class:list={["grid w-full gap-4 sm:grid-cols-2", className]}>
6
6
  <slot />
7
7
  </div>
@@ -5,10 +5,6 @@ const { lang = "text", title, class: className = "" } = Astro.props;
5
5
  const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
6
6
  const rawCode = decodeSlotText(slotHtml);
7
7
  const html = await highlightCode(rawCode, lang);
8
- const classes = [
9
- "relative my-6 w-full max-w-full overflow-hidden rounded-lg",
10
- className,
11
- ];
12
8
 
13
9
  function decodeSlotText(value) {
14
10
  return String(value || "")
@@ -23,17 +19,7 @@ function decodeSlotText(value) {
23
19
  }
24
20
  ---
25
21
 
26
- <div
27
- data-code-block
28
- data-code-title={title || undefined}
29
- class:list={classes}
30
- >
31
- {
32
- title && (
33
- <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
34
- <span class="mr-auto">{title}</span>
35
- </header>
36
- )
37
- }
22
+ <div class:list={["code-block", className]} data-code-title={title || undefined}>
23
+ {title && <header><span>{title}</span></header>}
38
24
  <Fragment set:html={html} />
39
25
  </div>