Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 5 additions & 3 deletions js/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
20 changes: 13 additions & 7 deletions js/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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' });
Expand Down
30 changes: 29 additions & 1 deletion js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ` <span>${successText}</span>`;
} 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);
});
}