diff --git a/qdrant-landing/themes/qdrant-2024/assets/css/core.scss b/qdrant-landing/themes/qdrant-2024/assets/css/core.scss index 20a417b87..2782c4398 100644 --- a/qdrant-landing/themes/qdrant-2024/assets/css/core.scss +++ b/qdrant-landing/themes/qdrant-2024/assets/css/core.scss @@ -113,3 +113,4 @@ body { @import 'partials/footer'; @import 'partials/not-found'; @import 'partials/go-to-top-button'; +@import 'partials/anchor-link'; diff --git a/qdrant-landing/themes/qdrant-2024/assets/css/partials/_anchor-link.scss b/qdrant-landing/themes/qdrant-2024/assets/css/partials/_anchor-link.scss new file mode 100644 index 000000000..6d3031d43 --- /dev/null +++ b/qdrant-landing/themes/qdrant-2024/assets/css/partials/_anchor-link.scss @@ -0,0 +1,36 @@ +// Bottom-of-screen "Copied" confirmation for the heading links added by +// assets/js/vendor/anchor.js. That script creates and toggles the element. +// Dark in both themes: a light chip is too bright against the dark theme. +.anchor-copied-toast { + position: fixed; + left: 50%; + bottom: 28px; + z-index: 1000; + transform: translate(-50%, 8px); + padding: 8px 14px; + border-radius: 4px; + font-size: 14px; + line-height: 1.4; + white-space: nowrap; + color: $neutral-98; + background: $neutral-30; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28); + opacity: 0; + pointer-events: none; + transition: + opacity 0.15s ease, + transform 0.15s ease; +} + +.anchor-copied-toast--visible { + opacity: 1; + transform: translate(-50%, 0); +} + +// Respect reduced motion preference +@media (prefers-reduced-motion: reduce) { + .anchor-copied-toast { + transition: none; + transform: translate(-50%, 0); + } +} diff --git a/qdrant-landing/themes/qdrant-2024/assets/js/vendor/anchor.js b/qdrant-landing/themes/qdrant-2024/assets/js/vendor/anchor.js index c67601977..195249e68 100644 --- a/qdrant-landing/themes/qdrant-2024/assets/js/vendor/anchor.js +++ b/qdrant-landing/themes/qdrant-2024/assets/js/vendor/anchor.js @@ -1,12 +1,59 @@ import AnchorJS from 'anchor-js'; +const TOAST_CLASS = 'anchor-copied-toast'; +const TOAST_VISIBLE_CLASS = 'anchor-copied-toast--visible'; +const TOAST_DURATION = 1800; + const anchors = new AnchorJS(); anchors.options.placement = 'left'; anchors.options.class = 'text-decoration-none'; +let toast = null; +let toastTimeout = null; + +// Kept empty until a copy happens, so the live region announces each one. +function createToast() { + toast = document.createElement('div'); + toast.className = TOAST_CLASS; + toast.setAttribute('aria-live', 'polite'); + document.body.appendChild(toast); +} + +function showToast() { + if (!toast) return; + + clearTimeout(toastTimeout); + toast.textContent = 'Copied'; + toast.classList.add(TOAST_VISIBLE_CLASS); + + toastTimeout = setTimeout(() => { + toast.classList.remove(TOAST_VISIBLE_CLASS); + toast.textContent = ''; + }, TOAST_DURATION); +} + document.addEventListener('DOMContentLoaded', function (event) { anchors.add('.qdrant-post__body :is(h1, h2, h3, h4, h5, h6)'); if (/documentation|articles|course/.test(window.location?.pathname)) { anchors.add('.documentation-article > :is(h1, h2, h3, h4, h5, h6)'); } + createToast(); +}); + +// Copy the heading URL on click, on top of the address bar update the link +// already does natively. Delegated, because anchors.add() injects the links +// after this script runs. +document.addEventListener('click', async function (event) { + const link = event.target.closest('.anchorjs-link'); + + if (!link) return; + + try { + // link.href is already absolute, and matches what the address bar gets. + await navigator.clipboard.writeText(link.href); + showToast(); + } catch (e) { + // Clipboard access needs a secure context; the link still works without it. + console.error(e); + } });