From 9a1b60fa2b2240617639e2ec58460f33ad44da04 Mon Sep 17 00:00:00 2001 From: Cedric Date: Thu, 1 Oct 2026 22:13:30 +0200 Subject: [PATCH] Back-to-top button in the status bar A small arrow button at the right end of the status bar, shown once the page has scrolled 400 px, scrolls smoothly back to the top (instantly when the system asks for reduced motion). Living in the fixed status bar, it never covers a log row or the details panel, on desktop and phones. Co-Authored-By: Claude Opus 5.5 (1M context) --- web/app.js | 11 +++++++++++ web/index.html | 3 +++ web/style.css | 4 ++++ 3 files changed, 18 insertions(+) diff --git a/web/app.js b/web/app.js index ab92a30..e9a77ec 100644 --- a/web/app.js +++ b/web/app.js @@ -126,6 +126,7 @@ const I18N = { hDivision: (s) => `interval ${s}`, hCapped: 'enlarged', hCappedTitle: 'The chosen division would exceed 300 intervals over this range: it has been enlarged.', hUnzoom: '× Reset zoom', unitDay: 'd', + toTop: 'Back to top', colRcv: 'Received', colMt: 'Message time', colSev: 'Severity', colHost: 'Host', colApp: 'App', colMsg: 'Message', colGrip: 'Drag to resize, double-click for the automatic width', resetCols: 'Reset column widths', colsReset: 'Column widths reset', @@ -254,6 +255,7 @@ const I18N = { hDivision: (s) => `intervalle ${s}`, hCapped: 'élargi', hCappedTitle: 'La division choisie dépasserait 300 intervalles sur cette plage : elle a été élargie.', hUnzoom: '× Annuler le zoom', unitDay: 'j', + toTop: 'Revenir en haut', colRcv: 'Réception', colMt: 'Heure message', colSev: 'Sévérité', colHost: 'Hôte', colApp: 'App', colMsg: 'Message', colGrip: 'Glisser pour redimensionner, double-clic pour la largeur automatique', resetCols: 'Réinitialiser les colonnes', colsReset: 'Largeurs de colonnes réinitialisées', @@ -1517,6 +1519,15 @@ $('#newPill').addEventListener('click', () => { }); window.addEventListener('scroll', debounce(() => { if (window.scrollY <= 80) flushPending(); }, 150), { passive: true }); +// "Back to top" button, shown once the page has scrolled a little. +const syncToTop = () => { $('#toTop').hidden = window.scrollY < 400; }; +window.addEventListener('scroll', syncToTop, { passive: true }); +$('#toTop').addEventListener('click', () => { + const smooth = !matchMedia('(prefers-reduced-motion: reduce)').matches; + window.scrollTo({ top: 0, behavior: smooth ? 'smooth' : 'auto' }); +}); +syncToTop(); + $('#live').addEventListener('click', () => { state.live = !state.live; store.set('live', state.live ? '1' : '0'); diff --git a/web/index.html b/web/index.html index 69a02d5..a8ea7f7 100644 --- a/web/index.html +++ b/web/index.html @@ -115,6 +115,9 @@
+
diff --git a/web/style.css b/web/style.css index 2592aa7..a9bc9c7 100644 --- a/web/style.css +++ b/web/style.css @@ -425,6 +425,10 @@ mark.hit { background: var(--hit); color: inherit; border-radius: 3px; padding: .conn.ok::before { color: var(--sev-info); } .conn.ko::before { color: var(--sev-err); } #stats .bad { color: var(--sev-err); } +/* "Back to top", at the right end of the status bar: never over a log row */ +.to-top { width: 24px; height: 24px; margin: -5px -6px -5px auto; flex: none; } /* no taller bar */ +.to-top svg { width: 16px; height: 16px; } +.to-top[hidden] { display: none; } .toast { position: fixed; bottom: 48px; left: 50%; transform: translateX(-50%); z-index: 50;