#260 (fix attempt 2): static-width copy feedback via .swapbtn
CI / test (pull_request) Successful in 23s
CI / docker (pull_request) Skipped

This commit is contained in:
fen
2026-09-17 17:13:28 -05:00
parent 2a55f50d87
commit 75264ee9f4
4 changed files with 24 additions and 17 deletions
+14
View File
@@ -586,6 +586,20 @@ a.admin-link:hover { color: var(--fg); text-decoration: underline; }
border-color: var(--ok); border-color: var(--ok);
} }
/* #260: static-width success feedback. Label and checkmark stack in one
grid cell, so the button is always as wide as the wider of the two and
never shifts on click. Feedback is a pure .ok class toggle. */
.swapbtn {
display: inline-grid;
}
.swapbtn > * {
grid-area: 1 / 1;
justify-self: center;
}
.swapbtn .swap-check { visibility: hidden; }
.swapbtn.ok .swap-check { visibility: visible; }
.swapbtn.ok .swap-label { visibility: hidden; }
/* headings: unified treatment (mirrors .side-section h3) */ /* headings: unified treatment (mirrors .side-section h3) */
.settings-head h1, .paste-title-bar h1, .head-row h1, .inner h1 { .settings-head h1, .paste-title-bar h1, .head-row h1, .inner h1 {
letter-spacing: -0.01em; letter-spacing: -0.01em;
+4 -6
View File
@@ -192,18 +192,16 @@ async function create() {
// button, password auto-unlock, then redirect to the paste. // button, password auto-unlock, then redirect to the paste.
function finishCreate(data) { function finishCreate(data) {
const url = location.origin + '/' + (data.custom_slug || data.id); const url = location.origin + '/' + (data.custom_slug || data.id);
showResult('<a href="' + url + '">' + url + '</a> <button class="btn btn-icon" id="result-copy" title="Copy URL" type="button">⧉</button>', 'ok'); // #260 attempt 2: .swapbtn markup — label and checkmark share one grid
// cell, so the button width is static and feedback is a class toggle.
showResult('<a href="' + url + '">' + url + '</a> <button class="btn btn-icon swapbtn" id="result-copy" title="Copy URL" type="button"><span class="swap-label">Copy</span><span class="swap-check">✓</span></button>', 'ok');
$('result').dataset.token = data.deletion_token || ''; $('result').dataset.token = data.deletion_token || '';
const copyBtn = document.getElementById('result-copy'); const copyBtn = document.getElementById('result-copy');
copyBtn.addEventListener('click', () => { copyBtn.addEventListener('click', () => {
try { try {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
copyBtn.classList.add('ok'); // in-place success feedback (#53) copyBtn.classList.add('ok'); // in-place success feedback (#53)
// #260: pin the pre-swap width so the wider/narrower glyph never setTimeout(() => copyBtn.classList.remove('ok'), 2000);
// shifts neighbouring elements; release it when the label restores.
copyBtn.style.minWidth = Math.ceil(copyBtn.getBoundingClientRect().width) + 'px';
copyBtn.textContent = '✓';
setTimeout(() => { copyBtn.classList.remove('ok'); copyBtn.textContent = '⧉'; copyBtn.style.minWidth = ''; }, 2000);
} catch(e) { toast('Copy failed', 'error'); } } catch(e) { toast('Copy failed', 'error'); }
}); });
// token carried via sessionStorage, never in the URL (#143) // token carried via sessionStorage, never in the URL (#143)
+4 -9
View File
@@ -18,17 +18,12 @@ function toggleStats() {
} }
function copyFeedback(btn) { function copyFeedback(btn) {
if (!btn) return; if (!btn) return;
if (!btn.dataset.label) btn.dataset.label = btn.textContent; // remember the original label (Copy/Link) // #260 attempt 2: .swapbtn stacks the label and checkmark in the same grid
// #260: keep the button width static during the feedback so surrounding // cell, so the button width is always the wider of the two and never moves.
// elements never jump. Pin the pre-swap width, release it on restore. // Feedback is a pure class toggle; no width pinning, no textContent swap.
if (!btn.dataset.pinned) {
btn.style.minWidth = Math.ceil(btn.getBoundingClientRect().width) + 'px';
btn.dataset.pinned = '1';
}
btn.classList.add('ok'); btn.classList.add('ok');
btn.textContent = '✓';
clearTimeout(btn._okh); clearTimeout(btn._okh);
btn._okh = setTimeout(() => { btn.classList.remove('ok'); btn.textContent = btn.dataset.label; }, 2000); btn._okh = setTimeout(() => btn.classList.remove('ok'), 2000);
} }
function copyContent(btn) { function copyContent(btn) {
navigator.clipboard.writeText(document.getElementById('raw-content').value) navigator.clipboard.writeText(document.getElementById('raw-content').value)
+2 -2
View File
@@ -8,8 +8,8 @@
<div class="spacer"></div> <div class="spacer"></div>
<button type="button" class="iconbtn wrap-toggle" title="Toggle line wrap" aria-pressed="false">Wrap</button> <button type="button" class="iconbtn wrap-toggle" title="Toggle line wrap" aria-pressed="false">Wrap</button>
<a class="iconbtn" href="/raw/{{.ID}}">Raw</a> <a class="iconbtn" href="/raw/{{.ID}}">Raw</a>
<a class="iconbtn" href="#" id="copy-link-btn">Link</a> <a class="iconbtn swapbtn" href="#" id="copy-link-btn"><span class="swap-label">Link</span><span class="swap-check"></span></a>
<a class="iconbtn" href="#" id="copy-btn">Copy</a> <a class="iconbtn swapbtn" href="#" id="copy-btn"><span class="swap-label">Copy</span><span class="swap-check"></span></a>
{{if .DeletionToken}}<a class="iconbtn danger" href="#" id="delete-btn">Delete</a>{{end}} {{if .DeletionToken}}<a class="iconbtn danger" href="#" id="delete-btn">Delete</a>{{end}}
</div> </div>
</div> </div>