From d4996b619f151b86f1065f39b482015dd7d18141 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 20:56:06 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20add=20inline=20clipbo?= =?UTF-8?q?ard=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a consistent inline feedback mechanism for all clipboard copy actions. - Add `copyFeedback` utility in `js/utils.js` to temporarily show 'Copied!' and update ARIA labels. - Apply feedback to assistant message actions, markdown code blocks, Share Modal, and Canvas. - Add closure delay to Share Modal to ensure feedback visibility. - Record pattern in Palette journal. Co-authored-by: anaxifty <240023404+anaxifty@users.noreply.github.com> --- .Jules/palette.md | 3 +++ js/chat.js | 8 +++++--- js/ui.js | 20 +++++++++++++------- js/utils.js | 30 +++++++++++++++++++++++++++++- 4 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 .Jules/palette.md 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); + }); }