Skip to content

Commit 1816fdf

Browse files
committed
feat(web): floating navbar and code copy buttons
- Float the navbar as a rounded blurred island on landing and docs - Match docs code block radius and padding to the landing blocks - Add copy buttons to landing and docs code blocks
1 parent b4d42d0 commit 1816fdf

6 files changed

Lines changed: 81 additions & 13 deletions

File tree

web/src/app/(site)/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export default function Home() {
7676
return (
7777
<main className="flex flex-1 flex-col">
7878
{/* Nav */}
79-
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
80-
<div className="mx-auto flex w-full max-w-5xl items-center justify-between px-6 py-4">
79+
<header className="sticky top-0 z-20 px-4 pt-4">
80+
<div className="mx-auto flex w-full max-w-6xl items-center justify-between rounded-2xl border bg-background/70 px-5 py-3 shadow-sm backdrop-blur-md">
8181
<div className="flex items-center gap-2.5">
8282
<Image src={asset("/webskrap-logo.png")} alt="WebSkrap" width={28} height={28} />
8383
<span className="font-semibold tracking-tight">WebSkrap</span>

web/src/app/docs/[[...slug]]/page.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import { notFound } from "next/navigation";
33
import { getDoc, getDocSlugs } from "@/lib/docs";
4+
import { CodeCopy } from "@/components/code-copy";
45

56
export const dynamicParams = false;
67

@@ -24,9 +25,12 @@ export default async function DocPage(props: DocPageProps) {
2425
if (!doc) notFound();
2526

2627
return (
27-
<article
28-
className="prose prose-neutral max-w-none dark:prose-invert prose-headings:scroll-mt-24 prose-pre:border prose-pre:bg-card prose-a:text-primary"
29-
dangerouslySetInnerHTML={{ __html: doc.html }}
30-
/>
28+
<>
29+
<article
30+
className="prose prose-neutral max-w-none dark:prose-invert prose-headings:scroll-mt-24 prose-pre:rounded-2xl prose-pre:border prose-pre:bg-card prose-pre:p-6 prose-a:text-primary"
31+
dangerouslySetInnerHTML={{ __html: doc.html }}
32+
/>
33+
<CodeCopy />
34+
</>
3135
);
3236
}

web/src/app/docs/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export default function DocsLayout({
1414
}) {
1515
return (
1616
<div className="flex min-h-full flex-col">
17-
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
18-
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
17+
<header className="sticky top-0 z-20 px-4 pt-4">
18+
<div className="mx-auto flex w-full max-w-6xl items-center justify-between rounded-2xl border bg-background/70 px-5 py-3 shadow-sm backdrop-blur-md">
1919
<Link href="/" className="flex items-center gap-2.5">
2020
<Image src={asset("/webskrap-logo.png")} alt="WebSkrap" width={28} height={28} />
2121
<span className="font-semibold tracking-tight">WebSkrap</span>

web/src/components/code-block.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { codeToHtml } from "shiki";
2+
import { CopyButton } from "@/components/copy-button";
23

34
interface CodeBlockProps {
45
code: string;
@@ -13,10 +14,13 @@ export async function CodeBlock({ code, lang }: CodeBlockProps) {
1314
});
1415

1516
return (
16-
<div
17-
className="overflow-x-auto rounded-2xl border bg-card p-6 text-sm leading-relaxed [&_pre]:bg-transparent [&_pre]:font-mono [&_code]:font-mono"
18-
// shiki output is trusted, build-time generated from static strings
19-
dangerouslySetInnerHTML={{ __html: html }}
20-
/>
17+
<div className="relative">
18+
<CopyButton text={code} />
19+
<div
20+
className="overflow-x-auto rounded-2xl border bg-card p-6 text-sm leading-relaxed [&_pre]:bg-transparent [&_pre]:font-mono [&_code]:font-mono"
21+
// shiki output is trusted, build-time generated from static strings
22+
dangerouslySetInnerHTML={{ __html: html }}
23+
/>
24+
</div>
2125
);
2226
}

web/src/components/code-copy.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
const COPY_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
6+
const CHECK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
7+
8+
// Docs HTML is server-rendered (dangerouslySetInnerHTML), so attach copy
9+
// buttons to each <pre> on the client after mount.
10+
export function CodeCopy() {
11+
useEffect(() => {
12+
const pres = document.querySelectorAll<HTMLPreElement>("article pre");
13+
pres.forEach((pre) => {
14+
if (pre.dataset.copyReady) return;
15+
pre.dataset.copyReady = "1";
16+
pre.classList.add("relative");
17+
const text = pre.innerText;
18+
19+
const btn = document.createElement("button");
20+
btn.type = "button";
21+
btn.setAttribute("aria-label", "Copy code");
22+
btn.innerHTML = COPY_SVG;
23+
btn.className =
24+
"absolute right-3 top-3 z-10 inline-flex size-7 items-center justify-center rounded-md border bg-background/80 text-muted-foreground backdrop-blur transition-colors hover:text-foreground";
25+
btn.addEventListener("click", async () => {
26+
await navigator.clipboard.writeText(text);
27+
btn.innerHTML = CHECK_SVG;
28+
setTimeout(() => {
29+
btn.innerHTML = COPY_SVG;
30+
}, 1500);
31+
});
32+
pre.appendChild(btn);
33+
});
34+
}, []);
35+
36+
return null;
37+
}

web/src/components/copy-button.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { Check, Copy } from "lucide-react";
5+
6+
export function CopyButton({ text }: { text: string }) {
7+
const [copied, setCopied] = React.useState(false);
8+
9+
return (
10+
<button
11+
type="button"
12+
aria-label="Copy code"
13+
onClick={async () => {
14+
await navigator.clipboard.writeText(text);
15+
setCopied(true);
16+
setTimeout(() => setCopied(false), 1500);
17+
}}
18+
className="absolute right-3 top-3 z-10 inline-flex size-7 items-center justify-center rounded-md border bg-background/80 text-muted-foreground backdrop-blur transition-colors hover:text-foreground [&_svg]:size-3.5"
19+
>
20+
{copied ? <Check /> : <Copy />}
21+
</button>
22+
);
23+
}

0 commit comments

Comments
 (0)