diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 389c78f..a18cc9e 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -299,8 +299,14 @@ html[data-wrap] .float { overflow-x: hidden; } .code-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); } .code { font-family: var(--font-mono); font-size: var(--code-fs); line-height: var(--code-lh); - padding: 14px 0; display: flex; overflow-x: auto; + padding: 14px 0; display: flex; overflow-x: hidden; } +/* #261: horizontal scroll must live on the codebody, not the .code flex + container — a container-level scroll takes the gutter with it when the + user scrolls long lines. The gutter sits OUTSIDE the scroll container and + stays visible; the codebody shrinks to the remaining space and scrolls + (min-width: 0 lets it shrink below its content width inside the flex row). */ +.code .codebody { flex: 1 1 auto; min-width: 0; overflow-x: auto; } /* #167: the gutter must not drive the flex layout — its content width (row count × number width) shrinks the code column, which re-wraps lines, which grows the gutter: a feedback loop. Pin the gutter with a fixed @@ -310,7 +316,7 @@ html[data-wrap] .float { overflow-x: hidden; } .code .gutter { flex-shrink: 0; } /* gutter/code share line metrics; the editor gutter keeps its own padding (#50) */ .code .gutter { padding-top: 0; padding-bottom: 0; } -.codebody { padding: 0 18px; white-space: pre; } +.codebody { padding: 0 18px; white-space: pre; overflow-x: auto; } /* #167: each logical line is its own block so offsetTop identifies its first visual row */ .codeline { display: block; } /* #167 rev: gutter number spans must stack one per visual row (wrap on) */ @@ -869,6 +875,9 @@ button[type="submit"]:focus-visible, .col-a { width: 260px; } .col-b { width: 140px; } .col-c { width: 120px; } .col-d { width: 96px; } .col-d2 { width: 150px; } .col-e { width: 190px; } .col-f { width: 100px; } .col-g { width: 190px; } +/* #255: history's URL column had its own narrow width (col-f doubles as + /mine's ID column); give it a dedicated class. */ +.col-url { width: 150px; } /* #210: /mine rows render a delete button cell that had no declared column, so under table-layout:fixed it overlapped the ID column. */ .col-del { width: 64px; } @@ -884,3 +893,17 @@ 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; } + +/* #267: jump to top/bottom pills for long pastes and the editor. + Hidden unless JS (jump.js) detects content more than 2x the viewport. */ +.jumpnav { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 50; + display: flex; + flex-direction: column; + gap: 8px; +} +.jumpnav.hidden { display: none; } +.jump-btn { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); } diff --git a/internal/web/static/jump.js b/internal/web/static/jump.js new file mode 100644 index 0000000..03fb611 --- /dev/null +++ b/internal/web/static/jump.js @@ -0,0 +1,41 @@ +/* #267: jump to top / bottom controls for long content. + Paste view scrolls the window; the /new editor scrolls its textarea. + The active scroller is chosen via data-jump-scroll on the script tag. */ +(function () { + var nav = document.getElementById('jumpnav'); + if (!nav) return; + var scroller = window; + var sel = nav.dataset.jumpScroll; + if (sel) scroller = document.querySelector(sel); + + function el() { + return scroller === window ? document.scrollingElement : scroller; + } + function isLarge() { + var e = el(); + if (!e) return false; + var visible = scroller === window ? window.innerHeight : e.clientHeight; + return e.scrollHeight > visible * 2; + } + function refresh() { + nav.classList.toggle('hidden', !isLarge()); + } + function jump(toTop) { + var e = el(); + if (!e) return; + if (scroller === window) { + window.scrollTo({ top: toTop ? 0 : e.scrollHeight }); + } else { + e.scrollTop = toTop ? 0 : e.scrollHeight; + } + } + nav.addEventListener('click', function (ev) { + var b = ev.target.closest('[data-jump]'); + if (!b) return; + ev.preventDefault(); + jump(b.dataset.jump === 'top'); + }); + window.addEventListener('resize', refresh); + if (scroller !== window && scroller) scroller.addEventListener('input', refresh); + refresh(); +})(); diff --git a/internal/web/templates/history.html b/internal/web/templates/history.html index 88359c7..2896a62 100644 --- a/internal/web/templates/history.html +++ b/internal/web/templates/history.html @@ -8,7 +8,7 @@
- + diff --git a/internal/web/templates/new.html b/internal/web/templates/new.html index ef90b5f..e21941f 100644 --- a/internal/web/templates/new.html +++ b/internal/web/templates/new.html @@ -33,6 +33,10 @@
+
@@ -90,4 +94,5 @@
+ {{template "foot" .}} diff --git a/internal/web/templates/paste.html b/internal/web/templates/paste.html index 4465632..f035074 100644 --- a/internal/web/templates/paste.html +++ b/internal/web/templates/paste.html @@ -55,8 +55,13 @@
{{.Gutter}}
{{.ContentHTML}}
{{end}} + + {{template "foot" .}}
Paste Type