diff --git a/docs/src/conf.py b/docs/src/conf.py index 354759a23..bde400535 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -149,6 +149,10 @@ html_favicon = 'static/favicon.ico' # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['static'] +# Custom CSS and JS files +html_css_files = ['copy-button.css'] +html_js_files = ['copy-button.js'] + # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. diff --git a/docs/src/static/copy-button.css b/docs/src/static/copy-button.css new file mode 100644 index 000000000..759f9a36f --- /dev/null +++ b/docs/src/static/copy-button.css @@ -0,0 +1,108 @@ +.code-block-wrapper { + position: relative; + margin: 0; + display: block; +} + +.copy-button { + position: absolute; + top: 0.5rem; + right: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem; + background-color: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 0.375rem; + color: #6b7280; + cursor: pointer; + transition: all 0.15s ease; + z-index: 10; + opacity: 0; + pointer-events: none; +} + +.code-block-wrapper:hover .copy-button, +.highlight-default:hover .copy-button { + opacity: 1; + pointer-events: auto; +} + +.copy-button:hover { + background-color: rgba(255, 255, 255, 0.95); + border-color: rgba(0, 0, 0, 0.2); + color: #374151; + transform: scale(1.1); +} + +.copy-button:active { + transform: scale(1.05); +} + +.copy-button:focus { + outline: 2px solid #3b82f6; + outline-offset: 2px; + opacity: 1; + pointer-events: auto; +} + +.copy-button:focus:not(:focus-visible) { + outline: none; +} + +.copy-button svg { + width: 1rem; + height: 1rem; + display: block; +} + +.copy-button.copied { + background-color: #10b981; + border-color: #10b981; + color: white; + opacity: 1; + pointer-events: auto; +} + +.copy-button.copied:hover { + background-color: #059669; + border-color: #059669; + transform: scale(1.1); +} + +@media (prefers-color-scheme: dark) { + .copy-button { + background-color: rgba(55, 65, 81, 0.7); + border-color: rgba(255, 255, 255, 0.15); + color: #d1d5db; + } + + .copy-button:hover { + background-color: rgba(55, 65, 81, 0.95); + border-color: rgba(255, 255, 255, 0.3); + color: #f3f4f6; + } +} + +@media (max-width: 640px) { + .copy-button { + opacity: 1; + pointer-events: auto; + } +} + +.code-block-wrapper div.highlight { + margin: 0; +} + +.code-block-wrapper > .highlight-default, +.code-block-wrapper > .highlight { + margin: 0; +} + +div.highlight-default.notranslate, +div.highlight-c.notranslate, +div.highlight-python.notranslate { + position: relative; +} diff --git a/docs/src/static/copy-button.js b/docs/src/static/copy-button.js new file mode 100644 index 000000000..b52da9f89 --- /dev/null +++ b/docs/src/static/copy-button.js @@ -0,0 +1,47 @@ +document.addEventListener('DOMContentLoaded', function() { + const codeBlocks = document.querySelectorAll('div.highlight'); + + codeBlocks.forEach(function(codeBlock) { + const parent = codeBlock.parentElement; + + if (parent && parent.classList.contains('code-block-wrapper')) { + return; + } + + const button = document.createElement('button'); + button.className = 'copy-button'; + button.type = 'button'; + button.setAttribute('aria-label', 'Copy code to clipboard'); + button.innerHTML = ''; + + button.addEventListener('click', function() { + const pre = codeBlock.querySelector('pre'); + const code = pre ? pre.textContent : codeBlock.textContent; + + navigator.clipboard.writeText(code).then(function() { + button.classList.add('copied'); + button.innerHTML = ''; + button.setAttribute('aria-label', 'Code copied to clipboard'); + + setTimeout(function() { + button.classList.remove('copied'); + button.innerHTML = ''; + button.setAttribute('aria-label', 'Copy code to clipboard'); + }, 2000); + }).catch(function(err) { + console.error('Failed to copy code: ', err); + }); + }); + + if (parent && (parent.classList.contains('highlight-default') || parent.classList.contains('highlight-c') || parent.classList.contains('highlight-python'))) { + parent.style.position = 'relative'; + parent.appendChild(button); + } else { + const wrapper = document.createElement('div'); + wrapper.className = 'code-block-wrapper'; + codeBlock.parentNode.insertBefore(wrapper, codeBlock); + wrapper.appendChild(button); + wrapper.appendChild(codeBlock); + } + }); +});