diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000..de3e09e --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2025-05-15 - Clipboard feedback implementation +**Learning:** Providing immediate inline feedback (e.g., changing button text to 'Copied!') in addition to toast notifications significantly improves user confidence in copy operations. When implementing this for buttons with SVG icons, using `innerHTML` and preserving the SVG's `outerHTML` ensures the visual style remains consistent during the feedback state. +**Action:** Use the `copyFeedback` utility pattern for all primary clipboard actions to ensure consistent interaction confirmation across the app. diff --git a/js/chat.js b/js/chat.js index f69099e..77f5662 100644 --- a/js/chat.js +++ b/js/chat.js @@ -384,10 +384,12 @@ function buildMsgActions(wrap, body, msg) { const actions = document.createElement('div'); actions.className = 'msg-actions'; - actions.appendChild(makeActionBtn('Copy', e => { + actions.appendChild(makeActionBtn('Copy', function (e) { e.stopPropagation(); - navigator.clipboard.writeText(body.innerText || body.textContent); - toast('Copied'); + navigator.clipboard.writeText(body.innerText || body.textContent).then(() => { + toast('Copied'); + copyFeedback(this); + }); })); actions.appendChild(makeActionBtn('↺ Rewrite', e => { e.stopPropagation(); diff --git a/js/ui.js b/js/ui.js index e86ed19..7e48282 100644 --- a/js/ui.js +++ b/js/ui.js @@ -607,10 +607,12 @@ document.addEventListener('keydown', e => { document.getElementById('canvas-close-btn').addEventListener('click', closeCanvas); document.getElementById('canvas-overlay').addEventListener('click', closeCanvas); -document.getElementById('canvas-copy-btn').addEventListener('click', () => { +document.getElementById('canvas-copy-btn').addEventListener('click', function () { const code = document.getElementById('canvas-body')?.querySelector('code'); - navigator.clipboard.writeText(code ? code.textContent : document.getElementById('canvas-body').innerText); - toast('Copied to clipboard'); + navigator.clipboard.writeText(code ? code.textContent : document.getElementById('canvas-body').innerText).then(() => { + toast('Copied to clipboard'); + copyFeedback(this); + }); }); document.getElementById('canvas-clear-output').addEventListener('click', () => { @@ -750,10 +752,14 @@ function openShareModal(msg) { document.getElementById('share-preview').textContent = transcript; document.getElementById('share-modal-overlay').classList.add('open'); - document.getElementById('share-copy-btn').onclick = () => { - navigator.clipboard.writeText(transcript); - toast('Chat copied to clipboard'); - document.getElementById('share-modal-overlay').classList.remove('open'); + document.getElementById('share-copy-btn').onclick = function () { + navigator.clipboard.writeText(transcript).then(() => { + toast('Chat copied to clipboard'); + copyFeedback(this); + setTimeout(() => { + document.getElementById('share-modal-overlay').classList.remove('open'); + }, 700); + }); }; document.getElementById('share-download-btn').onclick = () => { const blob = new Blob([transcript], { type: 'text/plain' }); diff --git a/js/utils.js b/js/utils.js index 5443126..d66cf9b 100644 --- a/js/utils.js +++ b/js/utils.js @@ -73,10 +73,38 @@ function readFileAsText(file) { }); } +// ── Clipboard feedback ───────────────────────────────────────────────────── +/** Provide temporary inline feedback on a button after a copy action */ +function copyFeedback(btn, successText = 'Copied!', delay = 2000) { + if (!btn || btn._copying) return; + btn._copying = true; + + const originalHTML = btn.innerHTML; + const originalAria = btn.getAttribute('aria-label'); + const svg = btn.querySelector('svg'); + + if (svg) { + btn.innerHTML = svg.outerHTML + ` ${successText}`; + } else { + btn.textContent = successText; + } + btn.setAttribute('aria-label', successText); + + setTimeout(() => { + btn.innerHTML = originalHTML; + if (originalAria) btn.setAttribute('aria-label', originalAria); + else btn.removeAttribute('aria-label'); + delete btn._copying; + }, delay); +} + // ── Code block actions ───────────────────────────────────────────────────── /** Copy the content of a code block to clipboard (called from inline onclick) */ function copyCodeBlock(btn) { const code = btn.closest('pre')?.querySelector('code'); if (!code) return; - navigator.clipboard.writeText(code.textContent).then(() => toast('Code copied')); + navigator.clipboard.writeText(code.textContent).then(() => { + toast('Code copied'); + copyFeedback(btn); + }); }