aboutsummaryrefslogtreecommitdiff
path: root/website/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-08-04 15:39:18 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-08-04 15:39:57 +0300
commit4cf7cff3a0f9a4b593f6cb189f0c1cc4378726f0 (patch)
treec42f339c4cc99307d40d44b05676592425f41013 /website/src
parente6fa5289f2de03bc7babf2ec07efef6b45ed398f (diff)
fix(website): copy heading links to clipboard
Diffstat (limited to 'website/src')
-rw-r--r--website/src/components/Head.astro57
-rw-r--r--website/src/styles/starlight.css32
2 files changed, 89 insertions, 0 deletions
diff --git a/website/src/components/Head.astro b/website/src/components/Head.astro
index 9987173b..dac83b81 100644
--- a/website/src/components/Head.astro
+++ b/website/src/components/Head.astro
@@ -8,3 +8,60 @@ const version = docsVersion(Astro.url.pathname);
{head.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)}
<meta data-pagefind-filter="version[content]" content={version} />
{version !== 'stable' && <meta name="robots" content="noindex, nofollow" />}
+
+<script>
+ const labels = (() => {
+ const lang = document.documentElement.lang.toLowerCase();
+ if (lang.startsWith('ja')) return { copy: 'リンクをコピー', copied: 'コピーしました' };
+ if (lang.startsWith('zh')) return { copy: '复制链接', copied: '已复制' };
+ return { copy: 'Copy link', copied: 'Copied' };
+ })();
+
+ function labelHeadingLinks(root: ParentNode = document) {
+ for (const link of root.querySelectorAll<HTMLAnchorElement>('.sl-anchor-link')) {
+ link.title = labels.copy;
+ const section = link.querySelector('.sr-only')?.textContent;
+ if (section) link.setAttribute('aria-label', `${labels.copy}: ${section}`);
+ }
+ }
+
+ labelHeadingLinks();
+ document.addEventListener('astro:page-load', () => labelHeadingLinks());
+ document.addEventListener('click', async (event) => {
+ if (
+ event instanceof MouseEvent &&
+ (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
+ ) {
+ return;
+ }
+
+ const target = event.target;
+ if (!(target instanceof Element)) return;
+ const link = target.closest('.sl-anchor-link');
+ if (!(link instanceof HTMLAnchorElement) || !navigator.clipboard?.writeText) return;
+
+ try {
+ await navigator.clipboard.writeText(link.href);
+ } catch {
+ return;
+ }
+
+ link.querySelector('.heading-link-copy-feedback')?.remove();
+ const feedback = document.createElement('span');
+ feedback.className = 'heading-link-copy-feedback';
+ feedback.setAttribute('aria-hidden', 'true');
+ feedback.textContent = labels.copied;
+ link.append(feedback);
+ window.setTimeout(() => feedback.remove(), 1400);
+
+ let status = document.querySelector<HTMLElement>('[data-heading-link-copy-status]');
+ if (!status) {
+ status = document.createElement('span');
+ status.className = 'sr-only';
+ status.setAttribute('role', 'status');
+ status.dataset.headingLinkCopyStatus = '';
+ document.body.append(status);
+ }
+ status.textContent = labels.copied;
+ });
+</script>
diff --git a/website/src/styles/starlight.css b/website/src/styles/starlight.css
index c7b82f67..39eef15f 100644
--- a/website/src/styles/starlight.css
+++ b/website/src/styles/starlight.css
@@ -182,6 +182,38 @@
color: var(--sl-color-accent);
}
+.sl-markdown-content .heading-link-copy-feedback {
+ position: absolute;
+ z-index: 10;
+ bottom: calc(100% + 0.4rem);
+ inset-inline-start: 50%;
+ padding: 0.25rem 0.45rem;
+ border: 1px solid var(--sl-color-hairline-light);
+ border-radius: 3px;
+ background: var(--sl-color-bg-nav);
+ color: var(--sl-color-white);
+ font-family: var(--sl-font);
+ font-size: var(--sl-text-xs);
+ font-weight: 500;
+ line-height: 1.2;
+ white-space: nowrap;
+ pointer-events: none;
+ transform: translateX(-50%);
+ animation: heading-link-copied 1.4s ease both;
+}
+
+@keyframes heading-link-copied {
+ 0% { opacity: 0; transform: translate(-50%, 0.25rem); }
+ 15%, 75% { opacity: 1; transform: translate(-50%, 0); }
+ 100% { opacity: 0; transform: translate(-50%, -0.15rem); }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .sl-markdown-content .heading-link-copy-feedback {
+ animation: none;
+ }
+}
+
.sl-markdown-content code:not(:where(.not-content *)) {
border: 1px solid var(--sl-color-hairline);
border-radius: 2px;