diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 389c78f..517540c 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -884,3 +884,22 @@ button[type="submit"]:focus-visible, .created-banner { display: block; } /* #167: gutter rows for wrapped paste view — one row per visual code line */ .gutline { display: block; } + +/* jump to top/bottom (#267): fixed pills bottom-right, only for tall content */ +.jump-nav { + position: fixed; right: 16px; bottom: 16px; + display: flex; flex-direction: column; gap: 8px; + opacity: 0; pointer-events: none; transform: translateY(6px); + transition: opacity .25s ease, transform .25s ease; z-index: 190; +} +.jump-nav.show { opacity: 1; pointer-events: auto; transform: translateY(0); } +.jump-nav button { + background: var(--surface-2); color: var(--fg); + border: 1px solid var(--border); border-radius: var(--radius-sm); + padding: 6px 14px; font-size: 13px; cursor: pointer; + box-shadow: 0 4px 16px rgba(0,0,0,.25); + appearance: none; font-family: inherit; +} +.jump-nav button:hover { border-color: var(--accent); color: var(--accent); } +.jump-nav button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } +.jump-nav button.hidden { display: none; } diff --git a/internal/web/static/jump.js b/internal/web/static/jump.js new file mode 100644 index 0000000..f1df8b2 --- /dev/null +++ b/internal/web/static/jump.js @@ -0,0 +1,53 @@ +// #267: jump to top / bottom for long content. +// Works on the paste view (page scroll) and the /new editor (textarea scroll). +// Buttons only appear when the content is tall enough to need them. +(function () { + const nav = document.querySelector('.jump-nav'); + if (!nav) return; + const topBtn = nav.querySelector('.jump-top'); + const botBtn = nav.querySelector('.jump-bottom'); + + // Target: the editor textarea on /new, else the window (paste view). + const editor = document.getElementById('content'); + const target = editor || window; + + function metrics() { + if (editor) { + return { + top: editor.scrollTop, + max: editor.scrollHeight - editor.clientHeight, + view: editor.clientHeight + }; + } + const doc = document.documentElement; + return { + top: window.scrollY, + max: doc.scrollHeight - window.innerHeight, + view: window.innerHeight + }; + } + + function refresh() { + const m = metrics(); + const tall = m.max > m.view * 1.5; + nav.classList.toggle('show', tall); + if (!tall) return; + // Hide the button for the edge you are already at. + topBtn.classList.toggle('hidden', m.top < 8); + botBtn.classList.toggle('hidden', m.max - m.top < 8); + } + + function jump(toTop) { + const y = toTop ? 0 : 99999999; + if (editor) editor.scrollTo({ top: toTop ? 0 : editor.scrollHeight, behavior: 'smooth' }); + else window.scrollTo({ top: toTop ? 0 : document.documentElement.scrollHeight, behavior: 'smooth' }); + } + + topBtn.addEventListener('click', () => jump(true)); + botBtn.addEventListener('click', () => jump(false)); + + target.addEventListener('scroll', refresh, { passive: true }); + if (editor) editor.addEventListener('input', refresh); + window.addEventListener('resize', refresh); + refresh(); +})(); diff --git a/internal/web/templates/new.html b/internal/web/templates/new.html index ef90b5f..033bd85 100644 --- a/internal/web/templates/new.html +++ b/internal/web/templates/new.html @@ -90,4 +90,9 @@ + +
+ + +
{{template "foot" .}} diff --git a/internal/web/templates/paste.html b/internal/web/templates/paste.html index 4465632..fdf6c53 100644 --- a/internal/web/templates/paste.html +++ b/internal/web/templates/paste.html @@ -56,7 +56,12 @@ {{end}} +
+ + +
+ {{template "foot" .}}