diff --git a/internal/web/static/app.css b/internal/web/static/app.css index a18cc9e..e97c58c 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -586,6 +586,20 @@ a.admin-link:hover { color: var(--fg); text-decoration: underline; } 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) */ .settings-head h1, .paste-title-bar h1, .head-row h1, .inner h1 { letter-spacing: -0.01em; diff --git a/internal/web/static/new.js b/internal/web/static/new.js index df2c191..0782762 100644 --- a/internal/web/static/new.js +++ b/internal/web/static/new.js @@ -192,18 +192,16 @@ async function create() { // button, password auto-unlock, then redirect to the paste. function finishCreate(data) { const url = location.origin + '/' + (data.custom_slug || data.id); - showResult('' + url + ' ', '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('' + url + ' ', 'ok'); $('result').dataset.token = data.deletion_token || ''; const copyBtn = document.getElementById('result-copy'); copyBtn.addEventListener('click', () => { try { navigator.clipboard.writeText(url); copyBtn.classList.add('ok'); // in-place success feedback (#53) - // #260: pin the pre-swap width so the wider/narrower glyph never - // 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); + setTimeout(() => copyBtn.classList.remove('ok'), 2000); } catch(e) { toast('Copy failed', 'error'); } }); // token carried via sessionStorage, never in the URL (#143) diff --git a/internal/web/static/paste.js b/internal/web/static/paste.js index 365f986..cc4fb4f 100644 --- a/internal/web/static/paste.js +++ b/internal/web/static/paste.js @@ -18,17 +18,12 @@ function toggleStats() { } function copyFeedback(btn) { if (!btn) return; - if (!btn.dataset.label) btn.dataset.label = btn.textContent; // remember the original label (Copy/Link) - // #260: keep the button width static during the feedback so surrounding - // elements never jump. Pin the pre-swap width, release it on restore. - if (!btn.dataset.pinned) { - btn.style.minWidth = Math.ceil(btn.getBoundingClientRect().width) + 'px'; - btn.dataset.pinned = '1'; - } + // #260 attempt 2: .swapbtn stacks the label and checkmark in the same grid + // cell, so the button width is always the wider of the two and never moves. + // Feedback is a pure class toggle; no width pinning, no textContent swap. btn.classList.add('ok'); - btn.textContent = '✓'; 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) { navigator.clipboard.writeText(document.getElementById('raw-content').value) diff --git a/internal/web/templates/paste.html b/internal/web/templates/paste.html index f035074..25a2d2e 100644 --- a/internal/web/templates/paste.html +++ b/internal/web/templates/paste.html @@ -8,8 +8,8 @@
Raw - Link - Copy + Link + Copy {{if .DeletionToken}}Delete{{end}}