feat: Mac-like scrollbars - thin, no track, shown while scrolling or at the edge and fading out, none when there is nothing to scroll; a lone terminal's bar sits by the frame, right of the plus
This commit is contained in:
1 parent
baee90b564
commit
25ffa60d09
10 files changed
+78
-5
No files matched your search
@@ -246,3 +246,16 @@ input.sw:disabled { opacity: .45; cursor: not-allowed; }
|
|||||||
table.stack td.actions { width: auto; padding-top: 8px; justify-content: flex-start; }
|
table.stack td.actions { width: auto; padding-top: 8px; justify-content: flex-start; }
|
||||||
table.stack td:empty { display: none; }
|
table.stack td:empty { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrollbars as on a Mac: thin, no track, seen only while scrolling or with
|
||||||
|
the pointer at the edge, then fading out (scroll.js sets --sb from 0 to 1).
|
||||||
|
Nothing to scroll, nothing shown. */
|
||||||
|
* { --sb: 0; }
|
||||||
|
::-webkit-scrollbar { width: 8px; height: 8px; background: transparent; }
|
||||||
|
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { border-radius: 4px; border: 2px solid transparent; background-clip: padding-box;
|
||||||
|
background-color: rgba(170, 180, 195, calc(var(--sb) * .5)); }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background-color: rgba(170, 180, 195, calc(var(--sb) * .75)); }
|
||||||
|
@supports not selector(::-webkit-scrollbar) {
|
||||||
|
* { scrollbar-width: thin; scrollbar-color: rgba(170, 180, 195, calc(var(--sb) * .5)) transparent; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Mac-like scrollbars: a scrolled element shows its thin scrollbar, and so
|
||||||
|
// does one whose edge the pointer is at; after a second at rest it fades out.
|
||||||
|
// Elements with nothing to scroll never show one. The look is in app.css.
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var live = new Map(); // element -> {v: shown 0..1, until: ms to stay}
|
||||||
|
var raf = 0;
|
||||||
|
function el(t) { return t === document || t === window ? document.documentElement : t; }
|
||||||
|
function scrollable(e) {
|
||||||
|
return e.scrollHeight > e.clientHeight + 1 || e.scrollWidth > e.clientWidth + 1;
|
||||||
|
}
|
||||||
|
function show(e, ms) {
|
||||||
|
if (!e || !e.style || !scrollable(e)) return;
|
||||||
|
var s = live.get(e) || {v: 0, until: 0};
|
||||||
|
s.until = Math.max(s.until, performance.now() + ms);
|
||||||
|
live.set(e, s);
|
||||||
|
if (!raf) raf = setTimeout(tick, 16);
|
||||||
|
}
|
||||||
|
var last = 0;
|
||||||
|
// A timer, not animation frames: those stop in a tab out of sight, and the
|
||||||
|
// bar would stay shown.
|
||||||
|
function tick() {
|
||||||
|
var now = performance.now();
|
||||||
|
var dt = Math.min(64, now - (last || now - 16));
|
||||||
|
last = now;
|
||||||
|
live.forEach(function (s, e) {
|
||||||
|
// In fast (about 0.12 s), out slowly (about 0.4 s).
|
||||||
|
var want = now < s.until ? 1 : 0;
|
||||||
|
s.v = want ? Math.min(1, s.v + dt / 120) : Math.max(0, s.v - dt / 400);
|
||||||
|
e.style.setProperty('--sb', s.v.toFixed(3));
|
||||||
|
if (!want && s.v === 0) { e.style.removeProperty('--sb'); live.delete(e); }
|
||||||
|
});
|
||||||
|
raf = live.size ? setTimeout(tick, 16) : 0;
|
||||||
|
if (!raf) last = 0;
|
||||||
|
}
|
||||||
|
document.addEventListener('scroll', function (ev) { show(el(ev.target), 900); }, {capture: true, passive: true});
|
||||||
|
// The pointer at a scrollable element's right or bottom edge shows its bar.
|
||||||
|
var moved = null;
|
||||||
|
document.addEventListener('pointermove', function (ev) {
|
||||||
|
if (ev.pointerType !== 'mouse') return;
|
||||||
|
if (!moved) requestAnimationFrame(function () {
|
||||||
|
var m = moved; moved = null;
|
||||||
|
for (var e = m.target; e && e.nodeType === 1; e = e.parentElement) {
|
||||||
|
if (!scrollable(e)) continue;
|
||||||
|
var st = getComputedStyle(e);
|
||||||
|
if (!/(auto|scroll)/.test(st.overflowY + st.overflowX) && e !== document.documentElement) continue;
|
||||||
|
var r = e === document.documentElement ? {right: innerWidth, bottom: innerHeight} : e.getBoundingClientRect();
|
||||||
|
if (m.x >= r.right - 14 || m.y >= r.bottom - 14) show(e, 700);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
moved = {target: ev.target, x: ev.clientX, y: ev.clientY};
|
||||||
|
}, {passive: true});
|
||||||
|
})();
|
||||||
@@ -10,8 +10,6 @@
|
|||||||
const touch = matchMedia('(pointer: coarse)').matches;
|
const touch = matchMedia('(pointer: coarse)').matches;
|
||||||
const desk = !touch; // drag and drop, splits: computers only
|
const desk = !touch; // drag and drop, splits: computers only
|
||||||
if (touch) document.body.classList.add('touch');
|
if (touch) document.body.classList.add('touch');
|
||||||
const ua = (navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform || navigator.userAgent;
|
|
||||||
if (/^Win/i.test(ua) || /Windows/.test(navigator.userAgent)) document.body.classList.add('win');
|
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
const statusEl = $('status'), area = $('area'), tablist = $('tablist'), addBtn = $('tabadd'),
|
const statusEl = $('status'), area = $('area'), tablist = $('tablist'), addBtn = $('tabadd'),
|
||||||
floatAdd = $('floatadd'), addMenu = $('addmenu'), hint = $('hint'), mark = $('tabmark');
|
floatAdd = $('floatadd'), addMenu = $('addmenu'), hint = $('hint'), mark = $('tabmark');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Машины - LPRA</title>
|
<title>Машины - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Изменить машину - LPRA</title>
|
<title>Изменить машину - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Файлы · {{.Agent.Name}} - LPRA</title>
|
<title>Файлы · {{.Agent.Name}} - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
<link rel="stylesheet" href="{{asset "codemirror/codemirror.css"}}">
|
<link rel="stylesheet" href="{{asset "codemirror/codemirror.css"}}">
|
||||||
<style>
|
<style>
|
||||||
.fm-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; }
|
.fm-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:12px; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Порты · {{.Agent.Name}} - LPRA</title>
|
<title>Порты · {{.Agent.Name}} - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
<style>
|
<style>
|
||||||
/* One line: fixed narrow fields, the label takes what is left. */
|
/* One line: fixed narrow fields, the label takes what is left. */
|
||||||
.frow { display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap; }
|
.frow { display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Настройки - LPRA</title>
|
<title>Настройки - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
<style>
|
<style>
|
||||||
/* An install command, with a button to copy it at its right. */
|
/* An install command, with a button to copy it at its right. */
|
||||||
/* Users: role, machines, password sign-in. */
|
/* Users: role, machines, password sign-in. */
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Статус · {{.Agent.Name}} - LPRA</title>
|
<title>Статус · {{.Agent.Name}} - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
<style>
|
<style>
|
||||||
.meters { display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:20px; margin-bottom:20px; }
|
.meters { display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:20px; margin-bottom:20px; }
|
||||||
.meters .card { margin:0; }
|
.meters .card { margin:0; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Терминал · {{.Agent.Name}} - LPRA</title>
|
<title>Терминал · {{.Agent.Name}} - LPRA</title>
|
||||||
<link rel="stylesheet" href="{{asset "app.css"}}">
|
<link rel="stylesheet" href="{{asset "app.css"}}">
|
||||||
<script src="{{asset "pin.js"}}"></script>
|
<script src="{{asset "pin.js"}}"></script>
|
||||||
|
<script src="{{asset "scroll.js"}}" defer></script>
|
||||||
<link rel="stylesheet" href="{{asset "xterm.css"}}">
|
<link rel="stylesheet" href="{{asset "xterm.css"}}">
|
||||||
<style>
|
<style>
|
||||||
#status.st-ok { color:#37c871; }
|
#status.st-ok { color:#37c871; }
|
||||||
@@ -78,9 +79,6 @@
|
|||||||
background:transparent; border:1px solid var(--border); opacity:.28; transition:opacity .25s ease, background .25s ease, top .25s ease; }
|
background:transparent; border:1px solid var(--border); opacity:.28; transition:opacity .25s ease, background .25s ease, top .25s ease; }
|
||||||
/* One tab split in panes: below the panes' title bars, clear of their close buttons. */
|
/* One tab split in panes: below the panes' title bars, clear of their close buttons. */
|
||||||
body.split1 .float-add { top:42px; }
|
body.split1 .float-add { top:42px; }
|
||||||
/* Windows draws a wide scrollbar in the terminal: the "+" keeps clear of it. */
|
|
||||||
body.win .float-add { right:38px; top:16px; }
|
|
||||||
body.win.split1 .float-add { top:48px; }
|
|
||||||
.float-add:hover, .float-add:focus-visible { opacity:1; background:var(--panel); }
|
.float-add:hover, .float-add:focus-visible { opacity:1; background:var(--panel); }
|
||||||
body.multi .float-add { opacity:0; pointer-events:none; }
|
body.multi .float-add { opacity:0; pointer-events:none; }
|
||||||
.tabview { position:absolute; inset:8px; opacity:0; visibility:hidden; transform:translateY(4px) scale(.995);
|
.tabview { position:absolute; inset:8px; opacity:0; visibility:hidden; transform:translateY(4px) scale(.995);
|
||||||
@@ -97,6 +95,10 @@
|
|||||||
.tabview.split .pane.active .pane-head { color:var(--text); }
|
.tabview.split .pane.active .pane-head { color:var(--text); }
|
||||||
.pane-title { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
.pane-title { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||||
.pane-body { flex:1; min-height:0; padding:4px 0 0 4px; }
|
.pane-body { flex:1; min-height:0; padding:4px 0 0 4px; }
|
||||||
|
/* A lone terminal's scrollbar sits in the margin by the frame, right of
|
||||||
|
the corner "+", not over the text. */
|
||||||
|
body:not(.touch) .tabview:not(.split) .pane { overflow:visible; }
|
||||||
|
body:not(.touch) .tabview:not(.split) .xterm .xterm-viewport { right:-8px; }
|
||||||
.pane.dragging { opacity:.45; }
|
.pane.dragging { opacity:.45; }
|
||||||
.gutter { position:absolute; z-index:3; }
|
.gutter { position:absolute; z-index:3; }
|
||||||
.gutter.c { cursor:col-resize; } .gutter.r { cursor:row-resize; }
|
.gutter.c { cursor:col-resize; } .gutter.r { cursor:row-resize; }
|
||||||
|
|||||||
Reference in new issue
Block a user