Skip to content

Commit b04d71c

Browse files
committed
feat: add Copy page dropdown to TOC sidebar
Adds a "Copy page" dropdown button at the top of the right-hand TOC sidebar with three options: - Copy page link: copies the current URL to clipboard - View Page as Markdown: opens the raw .md file in a new tab - Open in Claude: opens claude.ai pre-prompted to answer questions about the page using the markdown source URL
1 parent 1dd8b2d commit b04d71c

File tree

4 files changed

+165
-8
lines changed

4 files changed

+165
-8
lines changed

_components/CopyPage.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
export default function CopyPage({ file }: { file: string | undefined }) {
2+
if (!file || file.includes("[")) return null;
3+
4+
const markdownUrl = `https://docs.deno.com${file}`;
5+
const claudeUrl = `https://claude.ai/new?q=${
6+
encodeURIComponent(
7+
`Read this page from the Deno docs: ${markdownUrl} and answer questions about the content.`,
8+
)
9+
}`;
10+
11+
return (
12+
<details class="copy-page-dropdown mb-4">
13+
<summary class="btn list-none [&::-webkit-details-marker]:hidden flex items-center gap-2 cursor-pointer select-none">
14+
<svg
15+
xmlns="http://www.w3.org/2000/svg"
16+
aria-hidden="true"
17+
fill="currentColor"
18+
width="14"
19+
height="14"
20+
viewBox="0 0 16 16"
21+
>
22+
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" />
23+
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
24+
</svg>
25+
Copy page
26+
<svg
27+
class="copy-page-chevron ml-1 transition-transform duration-200"
28+
xmlns="http://www.w3.org/2000/svg"
29+
aria-hidden="true"
30+
fill="currentColor"
31+
width="12"
32+
height="12"
33+
viewBox="0 0 16 16"
34+
>
35+
<path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z" />
36+
</svg>
37+
</summary>
38+
39+
<div class="mt-2 border border-foreground-tertiary rounded-md overflow-hidden bg-white dark:bg-gray-900">
40+
<button
41+
class="copy-page-link-btn flex items-start gap-3 w-full px-4 py-3 text-left hover:bg-background-secondary dark:hover:bg-gray-800 transition-colors"
42+
>
43+
<svg
44+
xmlns="http://www.w3.org/2000/svg"
45+
aria-hidden="true"
46+
fill="currentColor"
47+
width="16"
48+
height="16"
49+
viewBox="0 0 16 16"
50+
class="mt-0.5 shrink-0"
51+
>
52+
<path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 2 2 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a2.002 2.002 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 2 2 0 0 0-2.83 0l-2.5 2.5a2.002 2.002 0 0 0 0 2.83Z" />
53+
</svg>
54+
<span>
55+
<span class="copy-page-link-label block text-sm font-medium">
56+
Copy page link
57+
</span>
58+
<span class="block text-xs text-foreground-secondary mt-0.5">
59+
Copy the current page URL to clipboard
60+
</span>
61+
</span>
62+
</button>
63+
64+
<a
65+
href={file}
66+
target="_blank"
67+
class="flex items-start gap-3 px-4 py-3 border-t border-foreground-tertiary hover:bg-background-secondary dark:hover:bg-gray-800 transition-colors"
68+
>
69+
<svg
70+
xmlns="http://www.w3.org/2000/svg"
71+
aria-hidden="true"
72+
fill="currentColor"
73+
width="16"
74+
height="16"
75+
viewBox="0 0 16 16"
76+
class="mt-0.5 shrink-0"
77+
>
78+
<path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5h-3.5a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-3.5a.75.75 0 0 1 1.5 0v3.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-8.5C2 2.784 2.784 2 3.75 2Zm6.854-1h4.146a.25.25 0 0 1 .25.25v4.146a.25.25 0 0 1-.427.177L13.03 4.03 9.28 7.78a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042l3.75-3.75-1.543-1.543A.25.25 0 0 1 10.604 1Z" />
79+
</svg>
80+
<span>
81+
<span class="block text-sm font-medium">View Page as Markdown</span>
82+
<span class="block text-xs text-foreground-secondary mt-0.5">
83+
Open the Markdown file in a new tab
84+
</span>
85+
</span>
86+
</a>
87+
88+
<a
89+
href={claudeUrl}
90+
target="_blank"
91+
class="flex items-start gap-3 px-4 py-3 border-t border-foreground-tertiary hover:bg-background-secondary dark:hover:bg-gray-800 transition-colors"
92+
>
93+
<svg
94+
xmlns="http://www.w3.org/2000/svg"
95+
aria-hidden="true"
96+
fill="currentColor"
97+
width="16"
98+
height="16"
99+
viewBox="0 0 24 24"
100+
class="mt-0.5 shrink-0"
101+
>
102+
<path d="M12 0C12 0 10.5 10.5 0 12C10.5 12 12 24 12 24C12 24 13.5 13.5 24 12C13.5 12 12 0 12 0Z" />
103+
</svg>
104+
<span>
105+
<span class="block text-sm font-medium">Open in Claude</span>
106+
<span class="block text-xs text-foreground-secondary mt-0.5">
107+
Ask Claude about this page
108+
</span>
109+
</span>
110+
</a>
111+
</div>
112+
</details>
113+
);
114+
}

_components/TableOfContents.tsx

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
import type { TableOfContentsItem as TableOfContentsItem_ } from "../types.ts";
22

3-
export default function TableOfContents({ data, toc, hasSubNav }: {
3+
export default function TableOfContents({ data, toc, hasSubNav, file }: {
44
data: Lume.Data;
55
toc: TableOfContentsItem_[];
66
hasSubNav: boolean;
7+
file?: string;
78
}) {
89
if (!toc || toc.length === 0) {
910
return null;
1011
}
1112
const topClasses = hasSubNav ? "top-header-plus-subnav" : "top-header";
1213

1314
return (
14-
<ul
15-
className={`toc-list hidden sticky p-4 pr-0 h-screen-minus-header overflow-y-auto border-l border-l-foreground-tertiary lg:block lg:w-full ${topClasses}`}
16-
id="toc"
15+
<div
16+
className={`hidden sticky ${topClasses} h-screen-minus-header border-l border-l-foreground-tertiary lg:flex lg:flex-col lg:w-full`}
1717
>
18-
{toc.map((item: TableOfContentsItem_) => (
19-
<data.comp.TableOfContentsItem item={item} />
20-
))}
21-
</ul>
18+
{file && !file.includes("[") && (
19+
<div class="px-4 pt-4 shrink-0">
20+
<data.comp.CopyPage file={file} />
21+
</div>
22+
)}
23+
<ul
24+
className="toc-list overflow-y-auto flex-1 p-4 pr-0"
25+
id="toc"
26+
>
27+
{toc.map((item: TableOfContentsItem_) => (
28+
<data.comp.TableOfContentsItem item={item} />
29+
))}
30+
</ul>
31+
</div>
2232
);
2333
}

_includes/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export default function Layout(data: Lume.Data) {
7777
<script type="module" defer src="/js/copy.js"></script>
7878
<script type="module" defer src="/js/tabs.js"></script>
7979
<script type="module" defer src="/js/feedback.js"></script>
80+
<script type="module" defer src="/js/copy-page.js"></script>
8081
<script type="module" defer src="/js/search.js"></script>
8182
<script
8283
async
@@ -119,6 +120,7 @@ export default function Layout(data: Lume.Data) {
119120
toc={data.toc}
120121
data={data}
121122
hasSubNav={hasSubNav}
123+
file={data.page?.sourcePath}
122124
/>
123125
)}
124126
</div>

js/copy-page.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Rotate chevron when dropdown opens/closes
2+
document.querySelectorAll<HTMLDetailsElement>(".copy-page-dropdown").forEach(
3+
(dropdown) => {
4+
dropdown.addEventListener("toggle", () => {
5+
const chevron = dropdown.querySelector<SVGElement>(".copy-page-chevron");
6+
if (chevron) {
7+
chevron.style.transform = dropdown.open ? "rotate(180deg)" : "";
8+
}
9+
});
10+
},
11+
);
12+
13+
// Handle "Copy page link" button
14+
document.querySelectorAll<HTMLButtonElement>(".copy-page-link-btn").forEach(
15+
(btn) => {
16+
btn.addEventListener("click", () => {
17+
navigator?.clipboard?.writeText(window.location.href).then(() => {
18+
const label = btn.querySelector<HTMLElement>(".copy-page-link-label");
19+
if (label) {
20+
const original = label.textContent;
21+
label.textContent = "Copied!";
22+
setTimeout(() => {
23+
label.textContent = original;
24+
}, 2000);
25+
}
26+
const dropdown = btn.closest<HTMLDetailsElement>(".copy-page-dropdown");
27+
if (dropdown) dropdown.open = false;
28+
});
29+
});
30+
},
31+
);

0 commit comments

Comments
 (0)