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
@@ -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">
@@ -4,49 +4,29 @@ 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";
7
8
  import PanelLeft from "lucide-static/icons/panel-left.svg?raw";
8
9
  import Search from "lucide-static/icons/search.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";
12
10
  import ClaudeLogo from "../../icons/logos/claude.svg?raw";
13
11
  import CursorLogo from "../../icons/logos/cursor.svg?raw";
14
12
  import MarkdownLogo from "../../icons/logos/markdown.svg?raw";
15
13
  import OpenAILogo from "../../icons/logos/openai.svg?raw";
16
14
  import BaseLayout from "./BaseLayout.astro";
17
15
  import CommandDialog from "./CommandDialog.astro";
16
+ import ContentHeader from "virtual:reallysimpledocs/components/ContentHeader";
18
17
  import DropdownMenu from "./DropdownMenu.astro";
19
18
  import Sidebar from "./Sidebar.astro";
20
- import { buildMenus, getBreadcrumbs, getNavigation } from "../lib/docs.js";
19
+ import ThemeToggle from "./ThemeToggle.astro";
20
+ import { buildMenus, getNavigation } from "../lib/docs.js";
21
21
  import { addSvgClass } from "../lib/html.js";
22
- import { renderDropdownMenu } from "../lib/macros.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
- const breadcrumbs = getBreadcrumbs(config, page);
30
27
  const { sidebar } = buildMenus(config, page.slug);
31
- const hasToc = headings.length > 0;
32
- const searchIndexUrl = routeFilePath(config.routeBase, "search-index.json");
33
- const chevronRightIcon = addSvgClass(ChevronRight, "size-3.5");
34
- const ellipsisIcon = addSvgClass(Ellipsis, "size-4");
35
28
  const panelLeftIcon = addSvgClass(PanelLeft, "size-4");
36
29
  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;
50
30
  const siteUrl = Astro.site?.toString().replace(/\/$/, "") || site.url || "";
51
31
  const pageUrl = `${siteUrl}${page.path}`;
52
32
  const llmPrompt = `Read from ${pageUrl} so I can ask questions about it.`;
@@ -83,11 +63,13 @@ const actionItems = [
83
63
  ---
84
64
 
85
65
  <BaseLayout title={page.title} description={site.description} pagePath={page.path}>
86
- <CommandDialog indexUrl={searchIndexUrl} />
66
+ <CommandDialog />
87
67
  <Sidebar menu={sidebar} />
88
68
 
89
69
  <main id="content">
90
- <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
+ >
91
73
  <div class="flex h-14 w-full items-center justify-between gap-2 px-4">
92
74
  <button
93
75
  type="button"
@@ -96,79 +78,43 @@ const actionItems = [
96
78
  data-tooltip="Toggle sidebar"
97
79
  data-side="bottom"
98
80
  data-align="start"
99
- class="btn-sm-icon-ghost"
81
+ class="btn"
82
+ data-variant="ghost"
83
+ data-size="icon-sm"
100
84
  >
101
85
  <Fragment set:html={panelLeftIcon} />
102
86
  </button>
103
87
 
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
-
150
- <button
151
- type="button"
152
- class="btn-sm-icon-ghost ml-auto sm:hidden"
153
- aria-label="Search"
154
- data-tooltip="Search"
155
- data-side="bottom"
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"
156
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(); }"
157
95
  >
158
- <Fragment set:html={searchIcon} />
159
- </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>
160
100
 
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>
101
+ <ContentHeader site={site} page={page} />
102
+ <ThemeToggle />
170
103
 
171
- <ContentHeader site={site} page={page} navigation={navigation} config={config} />
104
+ <div class="sm:hidden">
105
+ <button
106
+ type="button"
107
+ class="btn peer group"
108
+ data-variant="ghost"
109
+ data-size="icon-sm"
110
+ aria-label="Toggle sidebar"
111
+ aria-expanded="false"
112
+ aria-controls="sidebar"
113
+ onclick="document.getElementById('sidebar')?.toggle()"
114
+ >
115
+ <Fragment set:html={Menu} />
116
+ </button>
117
+ </div>
172
118
  </div>
173
119
  </header>
174
120
 
@@ -176,19 +122,20 @@ const actionItems = [
176
122
  <article class="min-w-0 flex-1 wrap-break-word">
177
123
  <div class="mx-auto w-full max-w-2xl flex-1 space-y-10">
178
124
  <header class="space-y-2">
179
- <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">
180
126
  <h1 class="mr-auto text-3xl font-semibold tracking-tight sm:text-4xl">{page.title}</h1>
181
- <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">
182
128
  <div role="group" class="button-group">
183
129
  <button
184
130
  type="button"
185
- class="btn-sm-secondary"
131
+ class="btn"
132
+ data-variant="secondary"
133
+ data-size="sm"
186
134
  data-md-url={page.markdownPath}
187
135
  onclick="copyPageMarkdown(this)"
188
136
  >
189
- <span data-copy-icon><Fragment set:html={Copy} /></span>
190
- <span data-check-icon class="hidden"><Fragment set:html={Check} /></span>
191
- <span>Copy page</span>
137
+ <Fragment set:html={Copy} />
138
+ Copy page
192
139
  </button>
193
140
  <hr role="separator" />
194
141
  <DropdownMenu items={actionItems} />
@@ -196,7 +143,7 @@ const actionItems = [
196
143
  <div role="group" class="button-group">
197
144
  {
198
145
  navigation.prev && (
199
- <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">
200
147
  <span class="sr-only">{navigation.prev.title}</span>
201
148
  <Fragment set:html={ArrowLeft} />
202
149
  </a>
@@ -205,7 +152,7 @@ const actionItems = [
205
152
  {navigation.prev && navigation.next && <hr role="separator" />}
206
153
  {
207
154
  navigation.next && (
208
- <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">
209
156
  <span class="sr-only">{navigation.next.title}</span>
210
157
  <Fragment set:html={ArrowRight} />
211
158
  </a>
@@ -221,7 +168,7 @@ const actionItems = [
221
168
  <footer class="mt-12 flex items-center">
222
169
  {
223
170
  navigation.prev && (
224
- <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">
225
172
  <Fragment set:html={ArrowLeft} />
226
173
  {navigation.prev.title}
227
174
  </a>
@@ -229,7 +176,7 @@ const actionItems = [
229
176
  }
230
177
  {
231
178
  navigation.next && (
232
- <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">
233
180
  {navigation.next.title}
234
181
  <Fragment set:html={ArrowRight} />
235
182
  </a>
@@ -238,51 +185,48 @@ const actionItems = [
238
185
  </footer>
239
186
  </div>
240
187
  </article>
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
- }
188
+ <aside class="hidden w-56 xl:block">
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>
190
+ <h2 class="flex items-center gap-1 font-medium">
191
+ On this page
192
+ </h2>
193
+ <ol>
194
+ {
195
+ headings.map((heading) => (
196
+ <li class={heading.depth === 3 ? "pl-3" : ""}>
197
+ <a href={`#${heading.id}`} data-toc-link>{heading.text}</a>
198
+ </li>
199
+ ))
200
+ }
201
+ </ol>
202
+ </div>
203
+ </aside>
259
204
  </div>
260
205
  </main>
261
206
 
262
- <script is:inline>
263
- async function copyPageMarkdown(button) {
207
+ <script is:inline define:vars={{ checkIcon: Check }}>
208
+ window.copyPageMarkdown = async function copyPageMarkdown(button) {
264
209
  const markdownUrl = button?.dataset?.mdUrl;
265
210
  if (!markdownUrl) return;
266
- const copyIcon = button.querySelector("[data-copy-icon]");
267
- const checkIcon = button.querySelector("[data-check-icon]");
211
+ const icon = button.querySelector("svg");
212
+ const originalIcon = icon?.outerHTML || "";
268
213
 
269
214
  try {
270
215
  const response = await fetch(markdownUrl, { credentials: "same-origin", cache: "no-store" });
271
216
  if (!response.ok) throw new Error("Failed to fetch markdown");
272
217
  const markdown = await response.text();
273
218
  await writeToClipboard(markdown);
274
- if (copyIcon && checkIcon) {
275
- copyIcon.classList.add("hidden");
276
- checkIcon.classList.remove("hidden");
219
+ if (icon && checkIcon) {
220
+ icon.outerHTML = checkIcon;
277
221
  window.setTimeout(() => {
278
- copyIcon.classList.remove("hidden");
279
- checkIcon.classList.add("hidden");
222
+ const currentIcon = button.querySelector("svg");
223
+ if (currentIcon && originalIcon) currentIcon.outerHTML = originalIcon;
280
224
  }, 1200);
281
225
  }
282
226
  } catch (_) {
283
227
  window.open(new URL(markdownUrl, window.location.href).toString(), "_blank", "noopener");
284
228
  }
285
- }
229
+ };
286
230
 
287
231
  async function writeToClipboard(text) {
288
232
  if (navigator.clipboard?.writeText && window.isSecureContext) {
@@ -305,55 +249,60 @@ const actionItems = [
305
249
 
306
250
  function initDocsToc() {
307
251
  const toc = document.querySelector("[data-docs-toc]");
308
- if (!toc) return;
309
-
310
- const links = Array.from(toc.querySelectorAll("[data-toc-link]"));
311
- const linkById = new Map();
312
- for (const link of links) {
313
- const id = decodeURIComponent(link.hash.slice(1));
314
- if (id) linkById.set(id, link);
315
- }
252
+ const links = Array.from(toc?.querySelectorAll("[data-toc-link]") || []);
253
+ if (!links.length) return;
316
254
 
317
- const headings = Array.from(linkById.keys())
318
- .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
+ })
319
261
  .filter(Boolean);
320
- if (!headings.length) return;
262
+ if (!entries.length) return;
321
263
 
322
- const setActive = (id) => {
323
- for (const link of links) {
324
- const active = decodeURIComponent(link.hash.slice(1)) === id;
325
- link.dataset.active = active ? "true" : "false";
326
- if (active) link.setAttribute("aria-current", "location");
327
- 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");
328
270
  }
329
271
  };
330
272
 
331
- const currentHeading = () => {
332
- const offset = 160;
333
- let current = headings[0];
334
- for (const heading of headings) {
335
- 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;
336
279
  else break;
337
280
  }
338
- return current;
339
- };
340
281
 
341
- const updateActive = () => setActive(currentHeading().id);
342
- updateActive();
282
+ activate(current.id);
283
+ };
343
284
 
344
- let queued = false;
345
- const queueUpdate = () => {
346
- if (queued) return;
347
- queued = true;
348
- window.requestAnimationFrame(() => {
349
- queued = false;
350
- updateActive();
285
+ let frame = 0;
286
+ const queueActiveUpdate = () => {
287
+ if (frame) return;
288
+ frame = window.requestAnimationFrame(() => {
289
+ frame = 0;
290
+ activeFromScroll();
351
291
  });
352
292
  };
353
293
 
354
- window.addEventListener("scroll", queueUpdate, { passive: true });
355
- window.addEventListener("resize", queueUpdate);
356
- 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));
357
306
  }
358
307
 
359
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
  />
@@ -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>
@@ -2,22 +2,9 @@
2
2
  import * as lucideIcons from "lucide-static";
3
3
  import { addSvgClass } from "../../lib/html.js";
4
4
 
5
- const { type, title = "Note", icon: iconName, action, class: className = "" } = Astro.props;
5
+ const { type, title = "Note", icon: iconName, class: className = "" } = Astro.props;
6
6
  const normalizedType = String(type || "").toLowerCase();
7
7
  const calloutType = normalizedType === "warning" || normalizedType === "destructive" ? normalizedType : "";
8
- const destructive = calloutType === "destructive";
9
- const actionLabel = action?.label;
10
- const actionClass = action?.class || "btn-sm-secondary";
11
- const actionAttrs = action
12
- ? {
13
- ...action.attrs,
14
- href: action.href,
15
- target: action.target,
16
- rel: action.rel,
17
- onclick: action.onclick,
18
- class: actionClass,
19
- }
20
- : {};
21
8
 
22
9
  function resolveNamedIcon(name) {
23
10
  if (!name || typeof name !== "string") return "";
@@ -36,29 +23,12 @@ function resolveNamedIcon(name) {
36
23
  const iconSource = typeof iconName === "string" ? resolveNamedIcon(iconName) : "";
37
24
  const icon = addSvgClass(iconSource, "size-4");
38
25
  const heading = title;
39
- const variantClass =
40
- calloutType === "warning"
41
- ? "[&>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"
42
- : destructive
43
- ? "[&>svg]:text-destructive [&>h3]:text-destructive [&>section]:text-destructive"
44
- : "";
45
26
  ---
46
27
 
47
- <div class:list={[destructive ? "alert-destructive" : "alert", "my-6", variantClass, className]}>
28
+ <div class:list={["alert", className]} data-variant={calloutType || undefined}>
48
29
  {icon && <Fragment set:html={icon} />}
49
30
  <h3>{heading}</h3>
50
31
  <section>
51
32
  <slot />
52
33
  </section>
53
- {
54
- actionLabel && (
55
- <footer>
56
- {action?.href ? (
57
- <a {...actionAttrs}>{actionLabel}</a>
58
- ) : (
59
- <button type="button" {...actionAttrs}>{actionLabel}</button>
60
- )}
61
- </footer>
62
- )
63
- }
64
34
  </div>
@@ -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>