feat: add code copy cutton

This commit is contained in:
radishzzz 2025-05-27 01:37:19 +01:00
parent 9c87c1bf03
commit 3312b30dbf
7 changed files with 132 additions and 5 deletions

View file

@ -15,6 +15,7 @@ import { visit } from 'unist-util-visit'
import UnoCSS from 'unocss/astro'
import { themeConfig } from './src/config'
import { langMap } from './src/i18n/config'
import { rehypeCodeCopyButton } from './src/plugins/rehype-code-copy-button.mjs'
import { rehypeImgToFigure } from './src/plugins/rehype-img-to-figure.mjs'
import { rehypeUnwrapImg } from './src/plugins/rehype-unwrap-img.mjs'
import { remarkAdmonitions } from './src/plugins/remark-admonitions.mjs'
@ -78,6 +79,7 @@ export default defineConfig({
rehypePlugins: [
rehypeKatex,
rehypeSlug,
rehypeCodeCopyButton,
rehypeImgToFigure,
rehypeUnwrapImg, // Must be after rehypeImgToFigure
[

View file

@ -0,0 +1,63 @@
<script>
const copyIcons = {
copy: `<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>`,
success: `<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>`,
}
// Store active timeouts to prevent memory leaks
const activeTimeouts = new WeakMap<HTMLButtonElement, ReturnType<typeof setTimeout>>()
async function handleCopy(button: HTMLButtonElement) {
const codeElement = button.parentElement?.querySelector('pre code')
const code = codeElement?.textContent || ''
try {
await navigator.clipboard.writeText(code)
// Clear existing timeout to prevent visual glitches on multiple clicks
const existingTimeout = activeTimeouts.get(button)
if (existingTimeout) {
clearTimeout(existingTimeout)
}
button.innerHTML = copyIcons.success
// Set timeout to revert to copy icon after 2 seconds
const timeoutId = setTimeout(() => {
button.innerHTML = copyIcons.copy
activeTimeouts.delete(button)
}, 1000)
activeTimeouts.set(button, timeoutId)
}
catch {
}
}
function setupCodeCopyButtons() {
// Only initialize buttons that haven't been initialized yet
document.querySelectorAll<HTMLButtonElement>('.code-copy-button:not([data-initialized])').forEach((button) => {
button.innerHTML = copyIcons.copy
button.setAttribute('data-initialized', 'true')
})
}
// Use event delegation for better performance
document.addEventListener('click', (e) => {
if (!(e.target instanceof HTMLElement))
return
// Find closest button element if clicked on button or child element
const button = e.target.closest('.code-copy-button') as HTMLButtonElement | null
if (button) {
handleCopy(button)
}
}, { passive: true })
setupCodeCopyButtons()
document.addEventListener('astro:page-load', setupCodeCopyButtons)
</script>

View file

@ -3,6 +3,7 @@ import Button from '@/components/Button.astro'
import Footer from '@/components/Footer.astro'
import Header from '@/components/Header.astro'
import Navbar from '@/components/Navbar.astro'
import CodeCopyButton from '@/components/Widgets/CodeCopyButton.astro'
import GithubCard from '@/components/Widgets/GithubCard.astro'
import GsapAnimation from '@/components/Widgets/GsapAnimation.astro'
import PhotoSwipe from '@/components/Widgets/PhotoSwipe.astro'
@ -44,7 +45,7 @@ const MarginBottom = isPost && themeConfig.comment?.enabled
<div
class="mx-auto max-w-205.848 min-h-vh w-full min-h-dvh"
p="x-[min(7.25vw,3.731rem)] y-10"
lg="mx-[max(5.75rem,calc(50vw-34.25rem))] my-20 max-w-[min(calc(75vw-16rem),44rem)] min-h-full p-0"
lg="mx-[max(5rem,calc(50vw-35rem))] my-20 max-w-[min(calc(75vw-16rem),44rem)] min-h-full p-0"
>
<Header />
<Navbar />
@ -55,7 +56,8 @@ const MarginBottom = isPost && themeConfig.comment?.enabled
</div>
{showAnimation && <GsapAnimation />}
<Button supportedLangs={supportedLangs} />
<GithubCard />
<CodeCopyButton />
<PhotoSwipe />
<GithubCard />
</body>
</html>

View file

@ -0,0 +1,37 @@
import { SKIP, visit } from 'unist-util-visit'
export function rehypeCodeCopyButton() {
return (tree) => {
visit(tree, 'element', (node, index, parent) => {
if (
node.tagName === 'pre'
&& node.children?.[0]?.tagName === 'code'
&& parent
&& !node.properties?.['data-copy-button-added']
) {
node.properties = node.properties || {}
node.properties['data-copy-button-added'] = 'true'
parent.children[index] = {
type: 'element',
tagName: 'div',
properties: { className: ['code-block-wrapper'] },
children: [
{
type: 'element',
tagName: 'button',
properties: {
'className': ['code-copy-button'],
'type': 'button',
'aria-label': 'Copy code',
},
children: [],
},
node,
],
}
return SKIP
}
})
}
}

View file

@ -139,6 +139,25 @@
--at-apply: 'mb-4';
}
/* Code Copy Button */
.code-copy-button {
--at-apply: 'z-99 absolute top-2.3 right-2.3 w-8 aspect-square uno-round-border border-secondary/15 c-secondary/80 cursor-pointer';
--at-apply: 'transition-opacity duration-300 ease-out op-100 bg-background lg:(op-0 bg-background)';
}
.code-block-wrapper:hover .code-copy-button {
--at-apply: 'op-100';
}
.code-copy-button:hover {
--at-apply: 'c-primary/80';
}
.code-copy-button svg {
--at-apply: 'w-4 h-4 block mx-auto';
}
.code-copy-button svg,
.code-copy-button svg path {
pointer-events: none;
}
/* :where(details) {
--at-apply: 'my-4 px-4 py-3 border border-solid border-secondary/25 rounded cursor-pointer';
}

View file

@ -2,6 +2,7 @@ html {
--at-apply: 'bg-background c-secondary antialiased';
scrollbar-width: thin;
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0.25) transparent;
scrollbar-gutter: stable both-edges;
}
/* Fix Flash Issue On iOS */

View file

@ -77,7 +77,7 @@
/* Code Blocks */
.heti :where(pre) {
--at-apply: 'my-6 overflow-auto uno-round-border px-4 py-3 bg-secondary/5!';
--at-apply: 'overflow-auto uno-round-border px-4 py-3 bg-secondary/5!';
scrollbar-width: thin;
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0) transparent;
transition: scrollbar-color 0.3s ease-out;
@ -91,10 +91,13 @@
html.dark .heti pre span {
--at-apply: 'text-[var(--shiki-dark)]!';
}
.heti :is(h1, h2, h3, h4, h5, h6, pre) + pre {
.heti .code-block-wrapper {
--at-apply: 'my-6 relative';
}
.heti :is(h1, h2, h3, h4, h5, h6, .code-block-wrapper) + .code-block-wrapper {
--at-apply: 'mt-4';
}
.heti pre:has(+ pre) {
.heti .code-block-wrapper:has(+ .code-block-wrapper) {
--at-apply: 'mb-4';
}