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
111 changes: 111 additions & 0 deletions _components/CopyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
export default function CopyPage({ file }: { file: string | undefined }) {
if (!file || file.includes("[")) return null;

const markdownUrl = `https://docs.deno.com${file}`;
const claudeUrl = `https://claude.ai/new?q=${
encodeURIComponent(
`Read this page from the Deno docs: ${markdownUrl} and answer questions about the content.`,
)
}`;

return (
<div class="copy-page-split inline-flex shrink-0 rounded-full border-2 border-foreground-primary dark:border-background-tertiary">
{/* Primary: directly copies the URL */}
<button
type="button"
class="copy-page-main-btn flex items-center gap-2 px-4 py-2 text-sm font-semibold text-foreground-primary bg-transparent hover:bg-header-highlight dark:hover:text-background-primary rounded-l-full transition-colors cursor-pointer select-none"
>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
width="12"
height="12"
viewBox="0 0 16 16"
>
<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" />
<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" />
</svg>
<span class="copy-page-main-label">Copy page</span>
</button>

{/* Visual divider between the two halves */}
<span
class="self-stretch w-0.5 bg-foreground-primary dark:bg-background-tertiary shrink-0"
aria-hidden="true"
>
</span>

{/* Chevron: opens the popover panel */}
<button
type="button"
class="copy-page-toggle-btn flex items-center px-3 py-2 text-foreground-primary bg-transparent hover:bg-header-highlight dark:hover:text-background-primary rounded-r-full transition-colors cursor-pointer select-none"
popovertarget="copy-page-menu"
aria-label="More copy options"
aria-haspopup="menu"
>
<svg
class="copy-page-chevron transition-transform duration-200"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
width="12"
height="12"
viewBox="0 0 16 16"
>
<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" />
</svg>
</button>

{/* Popover panel — lives in top layer, positioned via JS */}
<div id="copy-page-menu" popover="auto" class="copy-page-panel">
<a
href={file}
target="_blank"
class="no-underline flex items-start gap-3 px-4 py-3 hover:bg-background-secondary dark:hover:bg-gray-800 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 16 16"
class="mt-0.5 shrink-0"
>
<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" />
</svg>
<span>
<span class="block text-sm font-medium">View Page as Markdown</span>
<span class="block text-xs text-foreground-secondary mt-0.5">
Open the Markdown file in a new tab
</span>
</span>
</a>
<a
href={claudeUrl}
target="_blank"
class="no-underline 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"
>
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="currentColor"
width="16"
height="16"
viewBox="0 0 24 24"
class="mt-0.5 shrink-0"
>
<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" />
</svg>
<span>
<span class="block text-sm font-medium">Open in Claude</span>
<span class="block text-xs text-foreground-secondary mt-0.5">
Ask Claude about this page
</span>
</span>
</a>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion _components/TableOfContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function TableOfContents({ data, toc, hasSubNav }: {

return (
<ul
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}`}
className={`toc-list hidden sticky ${topClasses} h-screen-minus-header overflow-y-auto border-l border-l-foreground-tertiary p-4 pr-0 lg:flex lg:flex-col lg:w-full`}
id="toc"
>
{toc.map((item: TableOfContentsItem_) => (
Expand Down
18 changes: 18 additions & 0 deletions _config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,24 @@ site.addEventListener("afterBuild", async () => {
/* NOTE: we used to get gfm.css from the jsr.io CDN, but now we simply have a local copy. This is because it needs to be placed on a CSS layer, which isn't possible with an imported file. */
// Deno.writeTextFileSync(site.dest("gfm.css"), GFM_CSS);

// Copy lint rule markdown source files to _site so they're served at /lint/rules/*.md.
// These files are excluded from Lume's processing pipeline (site.ignore) because
// lint_rule.page.tsx handles page generation, but we still want the raw .md accessible.
try {
await Deno.mkdir(site.dest("lint/rules"), { recursive: true });
for await (const entry of Deno.readDir("lint/rules")) {
if (entry.isFile && entry.name.endsWith(".md")) {
await Deno.copyFile(
`lint/rules/${entry.name}`,
site.dest(`lint/rules/${entry.name}`),
);
}
}
log.info("Copied lint rule markdown files to _site/lint/rules/");
} catch (error) {
log.error("Error copying lint rule markdown files: " + error);
}

// Generate LLMs documentation files directly to _site directory
if (Deno.env.get("BUILD_TYPE") == "FULL") {
try {
Expand Down
17 changes: 11 additions & 6 deletions _includes/doc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,17 @@ export default function Doc(data: Lume.Data, helpers: Lume.Helpers) {
class="markdown-body mt-6 sm:mt-6"
>
{!(isReference && !isApiLandingPage) && (
<h1
dangerouslySetInnerHTML={{
__html: helpers.md(data.title!, true),
}}
>
</h1>
<header class="flex items-start justify-between gap-4">
<h1
dangerouslySetInnerHTML={{
__html: helpers.md(data.title!, true),
}}
>
</h1>
{file && !file.includes("[") && file.endsWith(".md") && (
<data.comp.CopyPage file={file} />
)}
</header>
)}
{data.available_since && (
<div class="bg-gray-200 rounded-md text-sm py-3 px-4 mb-4 font-semibold">
Expand Down
1 change: 1 addition & 0 deletions _includes/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default function Layout(data: Lume.Data) {
<script type="module" defer src="/js/copy.js"></script>
<script type="module" defer src="/js/tabs.js"></script>
<script type="module" defer src="/js/feedback.js"></script>
<script type="module" defer src="/js/copy-page.js"></script>
<script type="module" defer src="/js/search.js"></script>
<script
async
Expand Down
46 changes: 46 additions & 0 deletions js/copy-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Primary "Copy page" button — directly copies URL
document.querySelectorAll<HTMLButtonElement>(".copy-page-main-btn").forEach(
(btn) => {
btn.addEventListener("click", () => {
navigator?.clipboard?.writeText(window.location.href).then(() => {
const label = btn.querySelector<HTMLElement>(".copy-page-main-label");
if (label) {
const original = label.textContent;
label.textContent = "Copied!";
setTimeout(() => {
label.textContent = original;
}, 2000);
}
}).catch(() => {
const label = btn.querySelector<HTMLElement>(".copy-page-main-label");
if (label) {
const original = label.textContent;
label.textContent = "Copy failed";
setTimeout(() => {
label.textContent = original;
}, 2000);
}
});
});
},
);

// Popover panel — position below the chevron button + rotate chevron
const panel = document.getElementById("copy-page-menu") as HTMLElement | null;
const toggleBtn = document.querySelector<HTMLButtonElement>(
".copy-page-toggle-btn",
);

panel?.addEventListener("toggle", (event) => {
const e = event as ToggleEvent;
const chevron = toggleBtn?.querySelector<SVGElement>(".copy-page-chevron");

if (e.newState === "open" && toggleBtn) {
const rect = toggleBtn.getBoundingClientRect();
panel.style.top = `${rect.bottom + 4}px`;
panel.style.right = `${window.innerWidth - rect.right}px`;
}
if (chevron) {
chevron.style.transform = e.newState === "open" ? "rotate(180deg)" : "";
}
});
9 changes: 9 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,15 @@
}
}

.copy-page-panel {
/* Reset browser popover defaults */
inset: unset;
margin: 0;
padding: 0;
@apply w-72 rounded-md overflow-hidden bg-white dark:bg-gray-900 border
border-foreground-tertiary shadow-lg;
}

#feedback-no:focus + label,
#feedback-yes:focus + label {
@apply shadow-[inset_0_0_0_1px];
Expand Down