CopyLink

1 variant. Open one in isolation: use the link on each card.


src/ui/1_molecules/CopyLink.astro
---
interface Props {
permalink: string
}
const { permalink } = Astro.props
---
<button
class="copy-link-btn meta inline-flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 leading-none text-ink-muted transition-colors hover:text-ink select-none"
data-permalink={permalink}
aria-label="Copy link to clipboard"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
<span
class="copy-link-feedback text-xs"
style="opacity: 0; transition: opacity 0.2s ease;"
>Copied!</span>
</button>
<script>
document.querySelectorAll<HTMLButtonElement>('.copy-link-btn').forEach(btn => {
const feedback = btn.querySelector<HTMLElement>('.copy-link-feedback')
let timer: ReturnType<typeof setTimeout> | null = null
btn.addEventListener('click', async () => {
const permalink = btn.dataset.permalink
if (!permalink) return
try {
await navigator.clipboard.writeText(window.location.origin + permalink)
if (feedback) {
if (timer) clearTimeout(timer)
feedback.style.opacity = '1'
timer = setTimeout(() => {
if (feedback) feedback.style.opacity = '0'
}, 1500)
}
} catch {
// clipboard API unavailable (non-secure context or permission denied) — no feedback shown
}
})
})
</script>