Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@ import { defineConfig } from "astro/config";
import astroInference from "astro-inference";
import pagefind from "astro-pagefind";
import baseConfig from "./astro.base.config.mjs";
import { rehypeCodeCopy } from "./src/lib/rehype-code-copy.mjs";
import { remarkReadingTime } from "./src/lib/remark-reading-time.mjs";

const markdownProcessor = () =>
unified({
remarkPlugins: [remarkReadingTime],
rehypePlugins: [rehypeCodeCopy],
});

// https://astro.build/config
export default defineConfig({
site: "https://fastify.dev",
base: baseConfig.base,
outDir: "./build",
markdown: {
processor: unified({
remarkPlugins: [remarkReadingTime],
}),
processor: markdownProcessor(),
},
integrations: [
mdx({
processor: unified({
remarkPlugins: [remarkReadingTime],
}),
processor: markdownProcessor(),
}),
sitemap(),
pagefind(),
Expand Down
13 changes: 13 additions & 0 deletions src/assets/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions src/assets/icons/copy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
155 changes: 73 additions & 82 deletions src/components/CodeTabs.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
import { Code } from "astro:components";
import Icon from "~/components/Icon.astro";
import CopyButton from "~/components/CopyButton.astro";

interface Tab {
label: string;
Expand All @@ -14,90 +14,81 @@ interface Props {
}
const { tabs, title, id } = Astro.props;
---
<div class="overflow-hidden rounded-xl border border-border bg-surface" data-codetabs>
<div class="flex items-center justify-between border-b border-border bg-surface-2 px-2">
<div class="flex" role="tablist" aria-label={title}>
{tabs.map((tab, i) => (
<button
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-selected={i === 0 ? 'true' : 'false'}
aria-controls={`${id}-panel-${i}`}
data-tab={i}
class:list={[
'relative px-4 py-2.5 font-mono text-xs transition-colors',
i === 0 ? 'text-amber' : 'text-muted hover:text-fg',
]}
>
{tab.label}
<span
data-tab-underline
class:list={['absolute inset-x-2 -bottom-px h-px', i === 0 ? 'bg-amber' : 'bg-transparent']}
></span>
</button>
))}
</div>
<button
type="button"
data-copy
class="mr-1 flex items-center gap-1.5 rounded-md px-2.5 py-1.5 font-mono text-xs text-muted transition-colors hover:text-fg"
aria-label="Copy code"
>
<Icon name="copy" size={14} />
<span data-copy-label>Copy</span>
</button>
</div>
<div class="overflow-hidden rounded-xl border border-border bg-surface" data-codetabs data-copy-root={id}>
<div class="flex items-center justify-between border-b border-border bg-surface-2 px-2">
<div class="flex" role="tablist" aria-label={title}>
{tabs.map((tab, i) => (
<button
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-selected={i === 0 ? "true" : "false"}
aria-controls={`${id}-panel-${i}`}
data-tab={i}
class:list={[
"relative px-4 py-2.5 font-mono text-xs transition-colors",
i === 0 ? "text-amber" : "text-muted hover:text-fg",
]}
>
{tab.label}
<span
data-tab-underline
class:list={[
"absolute inset-x-2 -bottom-px h-px",
i === 0 ? "bg-amber" : "bg-transparent",
]}
/>
</button>
))}
</div>
<CopyButton
variant="label-swap"
ariaLabel="Copy code"
textSelector="[data-panel]:not(.hidden) [data-raw]"
class="mr-1 px-2.5 py-1.5 font-mono text-xs text-muted hover:text-fg"
/>
</div>

{tabs.map((tab, i) => (
<div
role="tabpanel"
id={`${id}-panel-${i}`}
aria-labelledby={`${id}-tab-${i}`}
data-panel={i}
class:list={['overflow-x-auto p-4 text-sm', i === 0 ? '' : 'hidden']}
>
<div class="hidden" data-raw set:text={tab.code}></div>
<Code code={tab.code} lang={tab.lang as any} themes={{ light: "github-light", dark: "github-dark" }} defaultColor="dark" />
</div>
))}
{tabs.map((tab, i) => (
<div
role="tabpanel"
id={`${id}-panel-${i}`}
aria-labelledby={`${id}-tab-${i}`}
data-panel={i}
class:list={["overflow-x-auto p-4 text-sm", i === 0 ? "" : "hidden"]}
>
<div class="hidden" data-raw set:text={tab.code} />
<Code
code={tab.code}
lang={tab.lang as any}
themes={{ light: "github-light", dark: "github-dark" }}
defaultColor="dark"
/>
</div>
))}
</div>

<script>
document.querySelectorAll('[data-codetabs]').forEach((root) => {
const tabs = root.querySelectorAll<HTMLButtonElement>('[data-tab]');
const panels = root.querySelectorAll<HTMLElement>('[data-panel]');
const underlines = root.querySelectorAll<HTMLElement>('[data-tab-underline]');
document.querySelectorAll("[data-codetabs]").forEach((root) => {
const tabs = root.querySelectorAll<HTMLButtonElement>("[data-tab]");
const panels = root.querySelectorAll<HTMLElement>("[data-panel]");
const underlines = root.querySelectorAll<HTMLElement>("[data-tab-underline]");

tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const idx = tab.dataset.tab;
tabs.forEach((t, i) => {
const active = String(i) === idx;
t.setAttribute('aria-selected', active ? 'true' : 'false');
t.classList.toggle('text-amber', active);
t.classList.toggle('text-muted', !active);
underlines[i]?.classList.toggle('bg-amber', active);
underlines[i]?.classList.toggle('bg-transparent', !active);
});
panels.forEach((p, i) => {
p.classList.toggle('hidden', String(i) !== idx);
});
});
});

const copyBtn = root.querySelector<HTMLButtonElement>('[data-copy]');
const label = root.querySelector('[data-copy-label]');
copyBtn?.addEventListener('click', async () => {
const visible = root.querySelector<HTMLElement>('[data-panel]:not(.hidden)');
const raw = visible?.querySelector('[data-raw]')?.textContent ?? '';
try {
await navigator.clipboard.writeText(raw);
if (label) {
label.textContent = 'Copied';
setTimeout(() => (label.textContent = 'Copy'), 1600);
}
} catch (e) {}
});
});
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
const idx = tab.dataset.tab;
tabs.forEach((t, i) => {
const active = String(i) === idx;
t.setAttribute("aria-selected", active ? "true" : "false");
t.classList.toggle("text-amber", active);
t.classList.toggle("text-muted", !active);
underlines[i]?.classList.toggle("bg-amber", active);
underlines[i]?.classList.toggle("bg-transparent", !active);
});
panels.forEach((p, i) => {
p.classList.toggle("hidden", String(i) !== idx);
});
});
});
});
</script>
56 changes: 56 additions & 0 deletions src/components/CopyButton.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
import CheckIcon from "~/assets/icons/check.svg";
import CopyIcon from "~/assets/icons/copy.svg";

interface Props {
text?: string;
textSelector?: string;
variant?: "icon-swap" | "label-swap";
copyLabel?: string;
copiedLabel?: string;
ariaLabel?: string;
class?: string;
}

const {
text,
textSelector,
variant = "icon-swap",
copyLabel = "Copy",
copiedLabel = "Copied",
ariaLabel = "Copy code",
class: className = "",
} = Astro.props;
---

<button
type="button"
data-copy-btn
data-copy-text={text}
data-copy-target={textSelector}
data-copy-label={copyLabel}
data-copied-label={copiedLabel}
aria-label={ariaLabel}
class:list={[
"inline-flex items-center gap-1.5 rounded-md transition-colors",
className,
]}
>
{
variant === "icon-swap" ? (
<>
<span data-copy-idle>
<CopyIcon width={14} height={14} aria-hidden="true" />
</span>
<span data-copy-done class="hidden text-amber">
<CheckIcon width={14} height={14} aria-hidden="true" />
</span>
</>
) : (
<>
<CopyIcon width={14} height={14} aria-hidden="true" />
<span data-copy-label-text>{copyLabel}</span>
</>
)
}
</button>
23 changes: 23 additions & 0 deletions src/components/DocsShell.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import { getCollection } from "astro:content";
import type { MarkdownHeading } from "astro";
import CopyButton from "~/components/CopyButton.astro";
import DocsNav from "~/components/DocsNav.astro";
import Icon from "~/components/Icon.astro";
import TableOfContents from "~/components/TableOfContents.astro";
Expand Down Expand Up @@ -293,6 +294,13 @@ const markdownUrl = withBase(`/docs/${markdownSlug}.md`);
</div>
</div>

<template data-docs-copy-template>
<CopyButton
ariaLabel="Copy code to clipboard"
class="docs-copy-btn"
/>
</template>

<script>
import { afterVersion, withBase } from "~/lib/href";

Expand Down Expand Up @@ -349,4 +357,19 @@ const markdownUrl = withBase(`/docs/${markdownSlug}.md`);

for (const heading of headings) observer.observe(heading);
}

const copyTemplate = document.querySelector<HTMLTemplateElement>(
"[data-docs-copy-template]",
);
const copySource =
copyTemplate?.content.querySelector<HTMLButtonElement>("[data-copy-btn]");
if (copySource) {
document
.querySelectorAll<HTMLElement>("article.prose-fastify .docs-pre-wrap")
.forEach((wrapper) => {
if (wrapper.querySelector(":scope > [data-copy-btn]")) return;
const button = copySource.cloneNode(true);
if (button instanceof HTMLButtonElement) wrapper.appendChild(button);
});
}
</script>
4 changes: 0 additions & 4 deletions src/components/Icon.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {
AlertTriangle,
ArrowRight,
Braces,
Check,
Clock,
Copy,
ExternalLink,
FileText,
Heart,
Expand Down Expand Up @@ -45,8 +43,6 @@ const lucide: Record<string, any> = {
search: Search,
sun: Sun,
moon: Moon,
copy: Copy,
check: Check,
external: ExternalLink,
menu: Menu,
close: X,
Expand Down
Loading
Loading